- Run tests for all 10 services via per-service pytest subprocesses - Group results by service with section headers - Clickable summary cards filter by outcome (passed/failed/errors/skipped) - Service filter nav using ~nav-link buttons in menu bar - Full menu integration: ~header-row + ~header-child + ~menu-row - Show logo image via cart-mini rendering - Mount full service directories in docker-compose for test access - Add 24 unit test files across 9 services Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
394 lines
13 KiB
Python
394 lines
13 KiB
Python
"""Unit tests for the Lexical JSON → HTML renderer."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from blog.bp.blog.ghost.lexical_renderer import (
|
|
render_lexical, _wrap_format, _align_style,
|
|
_FORMAT_BOLD, _FORMAT_ITALIC, _FORMAT_STRIKETHROUGH,
|
|
_FORMAT_UNDERLINE, _FORMAT_CODE, _FORMAT_SUBSCRIPT,
|
|
_FORMAT_SUPERSCRIPT, _FORMAT_HIGHLIGHT,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _wrap_format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestWrapFormat:
|
|
def test_no_format(self):
|
|
assert _wrap_format("hello", 0) == "hello"
|
|
|
|
def test_bold(self):
|
|
assert _wrap_format("x", _FORMAT_BOLD) == "<strong>x</strong>"
|
|
|
|
def test_italic(self):
|
|
assert _wrap_format("x", _FORMAT_ITALIC) == "<em>x</em>"
|
|
|
|
def test_strikethrough(self):
|
|
assert _wrap_format("x", _FORMAT_STRIKETHROUGH) == "<s>x</s>"
|
|
|
|
def test_underline(self):
|
|
assert _wrap_format("x", _FORMAT_UNDERLINE) == "<u>x</u>"
|
|
|
|
def test_code(self):
|
|
assert _wrap_format("x", _FORMAT_CODE) == "<code>x</code>"
|
|
|
|
def test_subscript(self):
|
|
assert _wrap_format("x", _FORMAT_SUBSCRIPT) == "<sub>x</sub>"
|
|
|
|
def test_superscript(self):
|
|
assert _wrap_format("x", _FORMAT_SUPERSCRIPT) == "<sup>x</sup>"
|
|
|
|
def test_highlight(self):
|
|
assert _wrap_format("x", _FORMAT_HIGHLIGHT) == "<mark>x</mark>"
|
|
|
|
def test_bold_italic(self):
|
|
result = _wrap_format("x", _FORMAT_BOLD | _FORMAT_ITALIC)
|
|
assert "<strong>" in result
|
|
assert "<em>" in result
|
|
|
|
def test_all_flags(self):
|
|
all_flags = (
|
|
_FORMAT_BOLD | _FORMAT_ITALIC | _FORMAT_STRIKETHROUGH |
|
|
_FORMAT_UNDERLINE | _FORMAT_CODE | _FORMAT_SUBSCRIPT |
|
|
_FORMAT_SUPERSCRIPT | _FORMAT_HIGHLIGHT
|
|
)
|
|
result = _wrap_format("x", all_flags)
|
|
for tag in ["strong", "em", "s", "u", "code", "sub", "sup", "mark"]:
|
|
assert f"<{tag}>" in result
|
|
assert f"</{tag}>" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _align_style
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestAlignStyle:
|
|
def test_no_format(self):
|
|
assert _align_style({}) == ""
|
|
|
|
def test_format_zero(self):
|
|
assert _align_style({"format": 0}) == ""
|
|
|
|
def test_left(self):
|
|
assert _align_style({"format": 1}) == ' style="text-align: left"'
|
|
|
|
def test_center(self):
|
|
assert _align_style({"format": 2}) == ' style="text-align: center"'
|
|
|
|
def test_right(self):
|
|
assert _align_style({"format": 3}) == ' style="text-align: right"'
|
|
|
|
def test_justify(self):
|
|
assert _align_style({"format": 4}) == ' style="text-align: justify"'
|
|
|
|
def test_string_format(self):
|
|
assert _align_style({"format": "center"}) == ' style="text-align: center"'
|
|
|
|
def test_unmapped_int(self):
|
|
assert _align_style({"format": 99}) == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# render_lexical — text nodes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRenderLexicalText:
|
|
def test_empty_doc(self):
|
|
assert render_lexical({"root": {"children": []}}) == ""
|
|
|
|
def test_plain_text(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "text", "text": "hello"}
|
|
]}}
|
|
assert render_lexical(doc) == "hello"
|
|
|
|
def test_html_escape(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "text", "text": "<script>alert('xss')</script>"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "<script>" not in result
|
|
assert "<script>" in result
|
|
|
|
def test_bold_text(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "text", "text": "bold", "format": _FORMAT_BOLD}
|
|
]}}
|
|
assert render_lexical(doc) == "<strong>bold</strong>"
|
|
|
|
def test_string_input(self):
|
|
import json
|
|
doc = {"root": {"children": [{"type": "text", "text": "hi"}]}}
|
|
assert render_lexical(json.dumps(doc)) == "hi"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# render_lexical — block nodes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRenderLexicalBlocks:
|
|
def test_paragraph(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "paragraph", "children": [
|
|
{"type": "text", "text": "hello"}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<p>hello</p>"
|
|
|
|
def test_empty_paragraph(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "paragraph", "children": []}
|
|
]}}
|
|
assert render_lexical(doc) == "<p><br></p>"
|
|
|
|
def test_heading_default(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "heading", "children": [
|
|
{"type": "text", "text": "title"}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<h2>title</h2>"
|
|
|
|
def test_heading_h3(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "heading", "tag": "h3", "children": [
|
|
{"type": "text", "text": "title"}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<h3>title</h3>"
|
|
|
|
def test_blockquote(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "quote", "children": [
|
|
{"type": "text", "text": "quoted"}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<blockquote>quoted</blockquote>"
|
|
|
|
def test_linebreak(self):
|
|
doc = {"root": {"children": [{"type": "linebreak"}]}}
|
|
assert render_lexical(doc) == "<br>"
|
|
|
|
def test_horizontal_rule(self):
|
|
doc = {"root": {"children": [{"type": "horizontalrule"}]}}
|
|
assert render_lexical(doc) == "<hr>"
|
|
|
|
def test_unordered_list(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "list", "listType": "bullet", "children": [
|
|
{"type": "listitem", "children": [
|
|
{"type": "text", "text": "item"}
|
|
]}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<ul><li>item</li></ul>"
|
|
|
|
def test_ordered_list(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "list", "listType": "number", "children": [
|
|
{"type": "listitem", "children": [
|
|
{"type": "text", "text": "one"}
|
|
]}
|
|
]}
|
|
]}}
|
|
assert render_lexical(doc) == "<ol><li>one</li></ol>"
|
|
|
|
def test_ordered_list_custom_start(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "list", "listType": "number", "start": 5, "children": []}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert 'start="5"' in result
|
|
|
|
def test_link(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "link", "url": "https://example.com", "children": [
|
|
{"type": "text", "text": "click"}
|
|
]}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert 'href="https://example.com"' in result
|
|
assert "click" in result
|
|
|
|
def test_link_xss_url(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "link", "url": 'javascript:alert("xss")', "children": []}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "javascript:alert("xss")" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# render_lexical — cards
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRenderLexicalCards:
|
|
def test_image(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "image", "src": "photo.jpg", "alt": "A photo"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-image-card" in result
|
|
assert 'src="photo.jpg"' in result
|
|
assert 'alt="A photo"' in result
|
|
assert 'loading="lazy"' in result
|
|
|
|
def test_image_wide(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "image", "src": "x.jpg", "cardWidth": "wide"}
|
|
]}}
|
|
assert "kg-width-wide" in render_lexical(doc)
|
|
|
|
def test_image_with_caption(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "image", "src": "x.jpg", "caption": "Caption text"}
|
|
]}}
|
|
assert "<figcaption>Caption text</figcaption>" in render_lexical(doc)
|
|
|
|
def test_codeblock(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "codeblock", "code": "print('hi')", "language": "python"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert 'class="language-python"' in result
|
|
assert "print('hi')" in result
|
|
|
|
def test_html_card(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "html", "html": "<div>raw</div>"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "<!--kg-card-begin: html-->" in result
|
|
assert "<div>raw</div>" in result
|
|
|
|
def test_markdown_card(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "markdown", "markdown": "**bold**"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "<!--kg-card-begin: markdown-->" in result
|
|
assert "<strong>bold</strong>" in result
|
|
|
|
def test_callout(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "callout", "backgroundColor": "blue",
|
|
"calloutEmoji": "💡", "children": [
|
|
{"type": "text", "text": "Note"}
|
|
]}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-callout-card-blue" in result
|
|
assert "💡" in result
|
|
|
|
def test_button(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "button", "buttonText": "Click",
|
|
"buttonUrl": "https://example.com", "alignment": "left"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-align-left" in result
|
|
assert "Click" in result
|
|
|
|
def test_toggle(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "toggle", "heading": "FAQ", "children": [
|
|
{"type": "text", "text": "Answer"}
|
|
]}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-toggle-card" in result
|
|
assert "FAQ" in result
|
|
|
|
def test_audio_duration(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "audio", "src": "a.mp3", "title": "Song", "duration": 185}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "3:05" in result
|
|
|
|
def test_audio_zero_duration(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "audio", "src": "a.mp3", "duration": 0}
|
|
]}}
|
|
assert "0:00" in render_lexical(doc)
|
|
|
|
def test_video(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "video", "src": "v.mp4", "loop": True}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-video-card" in result
|
|
assert " loop" in result
|
|
|
|
def test_file_size_kb(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "file", "src": "f.pdf", "fileName": "doc.pdf",
|
|
"fileSize": 512000} # 500 KB
|
|
]}}
|
|
assert "500 KB" in render_lexical(doc)
|
|
|
|
def test_file_size_mb(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "file", "src": "f.zip", "fileName": "big.zip",
|
|
"fileSize": 5242880} # 5 MB
|
|
]}}
|
|
assert "5.0 MB" in render_lexical(doc)
|
|
|
|
def test_paywall(self):
|
|
doc = {"root": {"children": [{"type": "paywall"}]}}
|
|
assert render_lexical(doc) == "<!--members-only-->"
|
|
|
|
def test_embed(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "embed", "html": "<iframe></iframe>",
|
|
"caption": "Video"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-embed-card" in result
|
|
assert "<figcaption>Video</figcaption>" in result
|
|
|
|
def test_bookmark(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "bookmark", "url": "https://example.com",
|
|
"metadata": {"title": "Example", "description": "A site"}}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-bookmark-card" in result
|
|
assert "Example" in result
|
|
|
|
def test_unknown_node_ignored(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "unknown-future-thing"}
|
|
]}}
|
|
assert render_lexical(doc) == ""
|
|
|
|
def test_product_stars(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "product", "productTitle": "Widget",
|
|
"rating": 3, "productDescription": "Nice"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-product-card" in result
|
|
assert result.count("kg-product-card-rating-active") == 3
|
|
|
|
def test_header_card(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "header", "heading": "Welcome",
|
|
"size": "large", "style": "dark"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-header-card" in result
|
|
assert "kg-size-large" in result
|
|
assert "Welcome" in result
|
|
|
|
def test_signup_card(self):
|
|
doc = {"root": {"children": [
|
|
{"type": "signup", "heading": "Subscribe",
|
|
"buttonText": "Join", "style": "light"}
|
|
]}}
|
|
result = render_lexical(doc)
|
|
assert "kg-signup-card" in result
|
|
assert "Join" in result
|