- 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
173 lines
7.6 KiB
Python
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
|