Files
chat/tests/test_render.py
T

111 lines
3.8 KiB
Python

"""Tests for the transcript renderer (Task 33).
Lightweight markdown for transcript turns:
- ``*action*`` → ``<em class="action">action</em>``
- ``**bold**`` → ``<strong>bold</strong>``
- ``((ooc))`` → ``<span class="ooc">((ooc))</span>``
- ``> line`` → ``<blockquote>line</blockquote>``
- paragraph breaks (double newline) → ``</p><p>``
- everything HTML-escaped first
No headings, no code blocks, no links — out of scope per Requirements §16.3.
"""
from __future__ import annotations
from chat.web.render import render_prose, render_turn_html
def test_render_prose_escapes_html():
"""Raw HTML in user content must be escaped — no XSS surface."""
out = render_prose("<script>alert(1)</script>")
assert "<script>" not in out
assert "&lt;script&gt;" in out
def test_render_prose_action_to_italic():
out = render_prose("*walks over*")
assert '<em class="action">walks over</em>' in out
def test_render_prose_bold_before_action():
"""Bold (``**``) must be processed before action (``*``)."""
out = render_prose("**emphasis** and *action*")
assert "<strong>emphasis</strong>" in out
assert '<em class="action">action</em>' in out
# Make sure we didn't double-wrap: no stray asterisks left behind.
assert "*" not in out
def test_render_prose_ooc_wrapped():
out = render_prose("((this is OOC))")
assert '<span class="ooc">' in out
assert "((this is OOC))" in out
def test_render_prose_paragraphs():
out = render_prose("First.\n\nSecond.")
# Two <p> opens and two closes.
assert out.count("<p>") == 2
assert out.count("</p>") == 2
assert "<p>First.</p>" in out
assert "<p>Second.</p>" in out
def test_render_prose_blockquote():
out = render_prose("> a quote")
assert "<blockquote>a quote</blockquote>" in out
def test_render_prose_empty():
"""Empty / whitespace-only inputs produce empty output, not stray tags."""
assert render_prose("") == ""
assert render_prose(" ") == ""
def test_render_turn_html_includes_role_class():
out = render_turn_html("BotA", "Hello.", role="bot")
assert 'class="turn turn-bot"' in out
assert "<strong>BotA</strong>" in out
assert "Hello." in out
def test_render_turn_html_escapes_speaker():
"""Speaker label is also HTML-escaped — names are user-controlled."""
out = render_turn_html("<bad>", "hi", role="you")
# Raw tag should not appear; escaped form should.
assert "<bad>" not in out
assert "&lt;bad&gt;" in out
def test_render_prose_mixed_full_message():
"""Realistic turn with action, dialogue, and an OOC aside."""
text = "*looks up* \"You're back late.\" ((she's tired))"
out = render_prose(text)
assert '<em class="action">looks up</em>' in out
# The apostrophe in ``she's`` is HTML-escaped to ``&#x27;``.
assert '<span class="ooc">((she&#x27;s tired))</span>' in out
def test_render_turn_html_stamps_event_id_when_provided():
"""T86 follow-up: when ``event_id`` is supplied the wrapper DIV
carries ``id="turn-<event_id>"`` so the chat-page
``turn_html_replace`` SSE handler can locate the prior turn DOM
node by id and swap it in-place. Without the id the handler's
``getElementById('turn-' + supersedes_id)`` lookup misses and
the regenerated turn appends instead of replaces.
"""
out = render_turn_html("BotA", "Hello.", role="bot", event_id=42)
assert 'id="turn-42"' in out
# The id must sit on the wrapper DIV, not somewhere nested inside.
assert out.startswith('<div id="turn-42" class="turn turn-bot">')
def test_render_turn_html_omits_id_when_event_id_missing():
"""Legacy callers (no ``event_id`` passed) get a clean DIV with no
id attribute — preserves the pre-T86 fragment shape.
"""
out = render_turn_html("BotA", "Hello.", role="bot")
assert "id=" not in out
assert out.startswith('<div class="turn turn-bot">')