Files
bruno 7b0dacabdd feat(export): v2.1.0 - export multi-formats HTML/MD-bundle/ePub
- backend/export.py: 3 exporters (HTML standalone, ZIP MD, ePub 3)
- HTML: CSS inliné, images base64, navigation wikilinks
- MD bundle: ZIP préserve structure répertoire
- ePub: zipfile + mistune, zero nouvelle dépendance
- 3 endpoints API: /api/export/{html,md-bundle,epub}
- UI: dropdown Export dans toolbar viewer (HTML/MD/ePub)
- 20 tests (validité, contenu, erreurs, API)
- 439 tests passent au total
2026-09-06 18:42:26 -04:00

173 lines
7.6 KiB
Python

# tests/test_export.py — Tests for multi-format export (HTML / MD bundle / ePub)
import io
import zipfile
from pathlib import Path
import pytest
from backend.export import (
ExportError,
export_epub,
export_html,
export_md_bundle,
)
# ═══════════════════════════════════════════════════════════════════
# export_html
# ═══════════════════════════════════════════════════════════════════
class TestExportHtml:
def test_valid_html(self, test_vault_dir):
vault = Path(test_vault_dir)
html = export_html(vault, vault / "note1.md")
assert isinstance(html, bytes)
text = html.decode("utf-8")
assert text.startswith("<!DOCTYPE html>")
assert "<html" in text
assert "</html>" in text
assert "<style>" in text # inlined CSS
def test_renders_content(self, test_vault_dir):
vault = Path(test_vault_dir)
html = export_html(vault, vault / "note1.md").decode("utf-8")
assert "Introduction" in html
assert "Python" in html
def test_uses_frontmatter_title(self, test_vault_dir):
vault = Path(test_vault_dir)
html = export_html(vault, vault / "note1.md").decode("utf-8")
assert "<title>Introduction à Python</title>" in html
def test_navigation_lists_other_notes(self, test_vault_dir):
vault = Path(test_vault_dir)
html = export_html(vault, vault / "note1.md").decode("utf-8")
assert "class=\"export-nav\"" in html
# note2 should appear as a sibling link
assert "note2.html" in html or "note2" in html
def test_wikilink_becomes_link(self, test_vault_dir):
vault = Path(test_vault_dir)
html = export_html(vault, vault / "note2.md").decode("utf-8")
assert "href=" in html
def test_missing_file_raises(self, test_vault_dir):
vault = Path(test_vault_dir)
with pytest.raises(ExportError):
export_html(vault, vault / "does_not_exist.md")
# ═══════════════════════════════════════════════════════════════════
# export_md_bundle
# ═══════════════════════════════════════════════════════════════════
class TestExportMdBundle:
def test_zip_contains_files(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_md_bundle(vault, vault)
assert isinstance(data, bytes)
zf = zipfile.ZipFile(io.BytesIO(data))
names = zf.namelist()
assert "note1.md" in names
assert "note2.md" in names
assert "config.json" in names
# Subdirectory preserved
assert any("Projets/projet.md" in n for n in names)
def test_zip_preserves_structure(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_md_bundle(vault, vault / "Projets")
zf = zipfile.ZipFile(io.BytesIO(data))
names = zf.namelist()
assert "projet.md" in names
def test_single_file_bundle(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_md_bundle(vault, vault / "note1.md")
zf = zipfile.ZipFile(io.BytesIO(data))
assert "note1.md" in zf.namelist()
def test_missing_path_raises(self, test_vault_dir):
vault = Path(test_vault_dir)
with pytest.raises(ExportError):
export_md_bundle(vault, vault / "nope")
# ═══════════════════════════════════════════════════════════════════
# export_epub
# ═══════════════════════════════════════════════════════════════════
class TestExportEpub:
def test_epub_is_valid_zip(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_epub(vault, vault / "note1.md")
assert isinstance(data, bytes)
zf = zipfile.ZipFile(io.BytesIO(data))
assert zf.testzip() is None # archive is intact
def test_epub_mimetype_first(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_epub(vault, vault / "note1.md")
zf = zipfile.ZipFile(io.BytesIO(data))
names = zf.namelist()
assert names[0] == "mimetype"
assert zf.read("mimetype") == b"application/epub+zip"
def test_epub_required_files(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_epub(vault, vault / "note1.md")
zf = zipfile.ZipFile(io.BytesIO(data))
names = set(zf.namelist())
assert "META-INF/container.xml" in names
assert "OEBPS/content.opf" in names
assert "OEBPS/toc.ncx" in names
assert "OEBPS/chapter1.xhtml" in names
def test_epub_content_rendered(self, test_vault_dir):
vault = Path(test_vault_dir)
data = export_epub(vault, vault / "note1.md")
zf = zipfile.ZipFile(io.BytesIO(data))
xhtml = zf.read("OEBPS/chapter1.xhtml").decode("utf-8")
assert "Introduction" in xhtml
assert "Python" in xhtml
def test_epub_missing_file_raises(self, test_vault_dir):
vault = Path(test_vault_dir)
with pytest.raises(ExportError):
export_epub(vault, vault / "nope.md")
# ═══════════════════════════════════════════════════════════════════
# API endpoints
# ═══════════════════════════════════════════════════════════════════
class TestExportApi:
def test_export_html_endpoint(self, client):
resp = client.get("/api/export/html", params={"vault": "TestVault", "path": "note1.md"})
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
assert "attachment" in resp.headers["content-disposition"]
assert resp.content.startswith(b"<!DOCTYPE html>")
def test_export_md_bundle_endpoint(self, client):
resp = client.get("/api/export/md-bundle", params={"vault": "TestVault", "path": ""})
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/zip"
zf = zipfile.ZipFile(io.BytesIO(resp.content))
assert "note1.md" in zf.namelist()
def test_export_epub_endpoint(self, client):
resp = client.get("/api/export/epub", params={"vault": "TestVault", "path": "note1.md"})
assert resp.status_code == 200
assert "epub" in resp.headers["content-type"]
zf = zipfile.ZipFile(io.BytesIO(resp.content))
assert zf.namelist()[0] == "mimetype"
def test_export_missing_file_returns_400(self, client):
resp = client.get("/api/export/html", params={"vault": "TestVault", "path": "nope.md"})
assert resp.status_code == 400
def test_export_unknown_vault_returns_404(self, client):
resp = client.get("/api/export/html", params={"vault": "NoVault", "path": "x.md"})
assert resp.status_code == 404