feat: chat — les posts affichent le markdown rendu comme un document, code coloré #193
This commit is contained in:
@@ -433,6 +433,30 @@ test("#110 — notification OS quand l'app est en arrière-plan", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- #193 : un post est rendu comme un document (markdown + code coloré) -----
|
||||
|
||||
test("#193 — post avec html rendu : markdown + bloc de code", () => {
|
||||
const list = document.createElement("div");
|
||||
renderChatMessages(list, [{
|
||||
id: "md1", user: "alice", text: "**gras**\n\n```python\nprint(1)\n```", ts: 400,
|
||||
html: '<p><strong>gras</strong></p><pre><code class="language-python">print(1)</code></pre>',
|
||||
}], "bruno");
|
||||
const body = list.querySelector("[data-id='md1'] .file-chat-body");
|
||||
assert.ok(body.classList.contains("file-chat-md"), "classe md posée sur le corps");
|
||||
assert.ok(body.querySelector(".md-content"), "conteneur md-content");
|
||||
assert.ok(body.querySelector(".md-content strong"), "markdown rendu (strong)");
|
||||
assert.ok(body.querySelector("pre code.language-python"), "bloc de code conservé");
|
||||
});
|
||||
|
||||
test("#193 — sans html, repli texte brut + URL cliquable", () => {
|
||||
const list = document.createElement("div");
|
||||
renderChatMessages(list, [{ id: "md2", user: "alice", text: "voir https://ex.com/a", ts: 401 }], "bruno");
|
||||
const body = list.querySelector("[data-id='md2'] .file-chat-body");
|
||||
assert.ok(!body.classList.contains("file-chat-md"), "pas de rendu markdown");
|
||||
assert.ok(body.querySelector("a.file-chat-link"), "URL toujours cliquable");
|
||||
assert.strictEqual(body.textContent, "voir https://ex.com/a", "texte intact");
|
||||
});
|
||||
|
||||
// Sequential execution: each case depends on panel state left by the previous.
|
||||
for (const [name, fn] of cases) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# tests/test_file_chat.py — Chat par fichier (#169)
|
||||
"""Store + REST routes for the per-file chat history."""
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
@@ -479,3 +480,73 @@ class TestReadReceipts:
|
||||
assert [e for e, _ in events] == ["chat_read"]
|
||||
assert events[0][1]["user"] == "anonymous"
|
||||
assert events[0][1]["vault"] == "__global__"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# #193 — un post est rendu comme un document markdown
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
class TestMarkdownRendering:
|
||||
"""Le post porte son HTML (mistune + sanitizer), comme une prévisualisation
|
||||
de document : le client n'a plus à réimplémenter le markdown."""
|
||||
|
||||
def test_message_carries_rendered_html(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(_store, "CHAT_DIR", tmp_path / "chats")
|
||||
msg = _store.add_message("V", "a.md", "u", "**gras** et `code`")
|
||||
assert "<strong>gras</strong>" in msg["html"]
|
||||
assert "<code>code</code>" in msg["html"]
|
||||
got = _store.get_messages("V", "a.md")
|
||||
assert "<strong>gras</strong>" in got[0]["html"]
|
||||
|
||||
def test_html_is_not_persisted(self, tmp_path, monkeypatch):
|
||||
"""`html` est recalculé à chaque lecture, jamais écrit dans le store."""
|
||||
chat_dir = tmp_path / "chats"
|
||||
monkeypatch.setattr(_store, "CHAT_DIR", chat_dir)
|
||||
_store.add_message("V", "a.md", "u", "**gras**")
|
||||
raw = json.loads(next(chat_dir.glob("*.json")).read_text(encoding="utf-8"))
|
||||
assert "html" not in raw["messages"][0]
|
||||
|
||||
def test_fenced_code_block_keeps_its_language(self, tmp_path, monkeypatch):
|
||||
"""La classe `language-xxx` est ce sur quoi hljs se branche (comme dans
|
||||
le viewer)."""
|
||||
monkeypatch.setattr(_store, "CHAT_DIR", tmp_path / "chats")
|
||||
msg = _store.add_message("V", "a.md", "u", "```python\nprint(1)\n```")
|
||||
assert 'class="language-python"' in msg["html"]
|
||||
|
||||
def test_tables_and_lists_are_rendered(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(_store, "CHAT_DIR", tmp_path / "chats")
|
||||
msg = _store.add_message("V", "a.md", "u", "- un\n- deux")
|
||||
assert "<ul>" in msg["html"] and "<li>un</li>" in msg["html"]
|
||||
|
||||
def test_raw_html_is_sanitized(self, tmp_path, monkeypatch):
|
||||
"""Pas de XSS par le chat : le sanitizer des documents s'applique."""
|
||||
monkeypatch.setattr(_store, "CHAT_DIR", tmp_path / "chats")
|
||||
msg = _store.add_message("V", "a.md", "u", "<script>alert(1)</script>ok")
|
||||
assert "<script" not in msg["html"]
|
||||
|
||||
def test_routes_return_html(self, client, test_vault_dir):
|
||||
r = client.post(
|
||||
"/api/file/TestVault/chat",
|
||||
json={"path": "note1.md", "text": "## Titre\n\n- item"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
html = r.json()["message"]["html"]
|
||||
assert "<h2" in html and "<li>item</li>" in html
|
||||
r2 = client.get("/api/file/TestVault/chat", params={"path": "note1.md"})
|
||||
assert "<h2" in r2.json()["messages"][0]["html"]
|
||||
|
||||
def test_sse_broadcast_carries_html(self, client, test_vault_dir, monkeypatch):
|
||||
"""L'écho SSE part avec le html : les autres clients n'attendent pas un
|
||||
refresh pour voir le rendu."""
|
||||
from backend.routers import file_chat as _router
|
||||
|
||||
events = []
|
||||
|
||||
class _Spy:
|
||||
async def broadcast(self, event, data):
|
||||
events.append((event, data))
|
||||
|
||||
monkeypatch.setattr(_router, "sse_manager", _Spy())
|
||||
client.post("/api/chat", json={"text": "**fort**"})
|
||||
assert events and events[0][0] == "chat_message"
|
||||
assert "<strong>fort</strong>" in events[0][1]["message"]["html"]
|
||||
|
||||
Reference in New Issue
Block a user