stackchain-dashboard/tests/test_markdown_renderer.py
timmy 26288d364c
All checks were successful
CI / lint (pull_request) Successful in 4m5s
CI / build-release (pull_request) Successful in 8s
CI / browser-journey (pull_request) Successful in 7m24s
CI / release-candidate (pull_request) Has been skipped
feat: recover progressive identity after partial snapshots
Closes #1431
2026-08-26 12:42:47 +00:00

260 lines
8.8 KiB
Python

import json
import re
import subprocess
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urljoin
FRONTEND = Path(__file__).parent.parent / "frontend"
RENDERER = FRONTEND / "markdown.js"
def render_markdown(payload, options=None):
options = options or {}
script = (
f"const render = require({json.dumps(str(RENDERER))});"
f"process.stdout.write(render({json.dumps(payload)}, {json.dumps(options)}));"
)
return subprocess.run(
["node", "-e", script],
check=True,
capture_output=True,
text=True,
).stdout
def toggle_task(payload, task_index, checked):
script = (
f"const render = require({json.dumps(str(RENDERER))});"
f"process.stdout.write(JSON.stringify(render.toggleTask({json.dumps(payload)}, "
f"{task_index}, {json.dumps(checked)})));"
)
return json.loads(
subprocess.run(
["node", "-e", script],
check=True,
capture_output=True,
text=True,
).stdout
)
def manage_task(payload, task_index, operation):
script = (
f"const manage = require({json.dumps(str(FRONTEND / 'issue-sheet.js'))}).manageChecklistTask;"
f"process.stdout.write(JSON.stringify(manage({json.dumps(payload)}, "
f"{task_index}, {json.dumps(operation)})));"
)
return json.loads(
subprocess.run(
["node", "-e", script],
check=True,
capture_output=True,
text=True,
).stdout
)
class ScriptSourceParser(HTMLParser):
def __init__(self):
super().__init__()
self.sources = []
def handle_starttag(self, tag, attrs):
if tag == "script":
source = dict(attrs).get("src")
if source:
self.sources.append(source)
def test_markdown_renderer_escapes_raw_html_before_rendering_heading():
payload = "# <img src=x onerror=alert(document.domain)>"
script = (
f"const render = require({json.dumps(str(RENDERER))});"
f"process.stdout.write(render({json.dumps(payload)}));"
)
result = subprocess.run(
["node", "-e", script],
check=True,
capture_output=True,
text=True,
)
assert result.stdout == (
"<h1>&lt;img src=x onerror=alert(document.domain)&gt;</h1>"
)
def test_markdown_script_resolves_inside_dashboard_subpath():
parser = ScriptSourceParser()
parser.feed((FRONTEND / "index.html").read_text())
markdown_source = next(
source for source in parser.sources if source.endswith("markdown.js")
)
assert urljoin(
"https://forge.alexanderwhitestone.com/dashboard/", markdown_source
) == "https://forge.alexanderwhitestone.com/dashboard/static/markdown.js"
def test_markdown_renderer_preserves_mobile_reading_structure():
rendered = render_markdown(
"## Plan\n\n> Ship this\n\n- [x] Tested\n- [ ] Released\n\n"
"```python\nprint('ready')\n```"
)
assert rendered == (
"<h2>Plan</h2><blockquote><p>Ship this</p></blockquote>"
'<ul class="task-list"><li class="task-list-item">'
'<input type="checkbox" disabled checked> Tested</li>'
'<li class="task-list-item"><input type="checkbox" disabled> Released</li></ul>'
'<pre><code class="language-python">print(&#39;ready&#39;)</code></pre>'
)
def test_interactive_markdown_tasks_expose_accessible_source_indices():
rendered = render_markdown(
"- [ ] Verify production\n- [x] Notify support",
{"interactiveTasks": True},
)
assert rendered == (
'<ul class="task-list"><li class="task-list-item">'
'<input type="checkbox" class="task-list-toggle" data-task-index="0" '
'aria-label="Mark Verify production complete"> Verify production</li>'
'<li class="task-list-item"><input type="checkbox" class="task-list-toggle" '
'data-task-index="1" aria-label="Mark Notify support incomplete" checked> '
'Notify support</li></ul>'
)
def test_manageable_markdown_tasks_expose_touch_action_with_position_boundaries():
rendered = render_markdown(
"- [ ] Verify production\n- [x] Notify support",
{"interactiveTasks": True, "manageTasks": True},
)
assert 'class="task-list-manage" data-task-index="0"' in rendered
assert 'aria-label="Manage step: Verify production"' in rendered
assert 'data-task-first="true"' in rendered
assert 'class="task-list-manage" data-task-index="1"' in rendered
assert 'data-task-last="true"' in rendered
def test_interactive_task_toggle_changes_exact_source_marker_only():
body = (
"```md\r\n- [ ] example only\r\n```\r\n"
"- [ ] Duplicate\r\n - [X] Nested duplicate\r\n- [ ] Duplicate\r\n"
)
assert toggle_task(body, 1, True) == (
"```md\r\n- [ ] example only\r\n```\r\n"
"- [ ] Duplicate\r\n - [X] Nested duplicate\r\n- [x] Duplicate\r\n"
)
def test_manage_task_renames_exact_visible_task_and_preserves_nested_step_line_endings_and_fences():
body = (
"```md\r\n- [ ] example only\r\n```\r\n"
"- [ ] Build\r\n - [X] Verify rollback\r\n- [ ] Release\r\n"
)
assert manage_task(body, 1, {"action": "rename", "label": "Ship"}) == (
"```md\r\n- [ ] example only\r\n```\r\n"
"- [ ] Build\r\n - [X] Verify rollback\r\n- [ ] Ship\r\n"
)
def test_manage_task_removes_only_the_selected_task_line():
body = "Plan\n- [ ] Build\n- [x] Test\n\nNotes"
assert manage_task(body, 0, {"action": "remove"}) == "Plan\n- [x] Test\n\nNotes"
def test_manage_task_moves_a_step_within_its_list_without_changing_step_content():
body = "Plan\n- [ ] Build\n- [x] Test\n- [ ] Release\n\nNotes"
assert manage_task(body, 2, {"action": "move-earlier"}) == (
"Plan\n- [ ] Build\n- [ ] Release\n- [x] Test\n\nNotes"
)
assert manage_task(body, 0, {"action": "move-later"}) == (
"Plan\n- [x] Test\n- [ ] Build\n- [ ] Release\n\nNotes"
)
def test_manage_task_rejects_an_empty_or_duplicate_renamed_label():
script = f"""
const manage = require({json.dumps(str(FRONTEND / 'issue-sheet.js'))}).manageChecklistTask;
const body = '- [ ] Build\\n- [x] Test';
const errors = ['', ' test '].map(label => {{
try {{ manage(body, 0, {{action:'rename',label}}); return null; }}
catch (error) {{ return error.message; }}
}});
process.stdout.write(JSON.stringify(errors));
"""
assert json.loads(subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
).stdout) == ["Enter a checklist step.", "That checklist step already exists."]
def test_markdown_renderer_allows_only_safe_links_and_keeps_html_inert():
rendered = render_markdown(
"[Forge](https://forge.example/work?q=1&safe=yes) "
"[relative](/git/org/repo/issues/1) "
"[attack](javascript:alert(1)) <img src=x onerror=alert(2)>"
)
assert (
'<a href="https://forge.example/work?q=1&amp;safe=yes" target="_blank" '
'rel="noopener noreferrer">Forge</a>' in rendered
)
assert (
'<a href="/git/org/repo/issues/1" target="_blank" '
'rel="noopener noreferrer">relative</a>' in rendered
)
assert "javascript:" not in rendered
assert "<img" not in rendered
assert "&lt;img src=x onerror=alert(2)&gt;" in rendered
def test_all_read_only_work_bodies_use_the_shared_markdown_renderer():
dashboard = (FRONTEND / "dashboard.js").read_text() + (FRONTEND / "conversation.js").read_text()
expected_paths = (
"renderMarkdown(comment.body || 'No comment body provided.')",
"renderMarkdown(detail.subject_body || 'No subject context was provided.')",
"renderMarkdown(detail.body || 'No description provided.')",
"renderMarkdown(item.body || 'No description provided.')",
"renderMarkdown(review.body)",
"issueController.renderTasks(qs('#issue-sheet-body'), detail,",
)
for path in expected_paths:
assert path in dashboard
assert dashboard.count("renderMarkdown(detail.body || 'No description provided.')") == 3
def test_markdown_work_bodies_are_mobile_safe_block_containers():
html = (FRONTEND / "index.html").read_text()
css = (FRONTEND / "dashboard.css").read_text()
worker = (FRONTEND / "service-worker.js").read_text()
for body_id in (
"issue-sheet-body",
"pull-sheet-body",
"review-sheet-body",
"update-subject-body",
"search-preview-body",
):
body_tag = re.search(rf'<div\b[^>]*\bid="{body_id}"[^>]*>', html)
assert body_tag
assert "markdown-content" in body_tag.group(0)
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v144" in worker