Files
ObsiGate/tests/test_excalidraw.py
T
bruno ac16fc1f0e
CI / lint (push) Successful in 52s
CI / security (push) Successful in 36s
CI / test (push) Successful in 1m6s
CI / build (push) Successful in 1m15s
CI / e2e (push) Failing after 12m40s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
feat: #78 Excalidraw finitions + #67 push + #68 health-detailed + #77 desktop jumplist
#78 Excalidraw — finitions B5/C8/F2/F3
- backend/indexer.py: port Python pur de lz-string decompressFromBase64 (bitsPerChar=6, resetValue=32) validé contre 4 fixtures JS truth (texte accentué, edge cases) — remplace le stub base64 non-fonctionnel
- B5 indexation: .excalidraw.md (format plugin Obsidian) maintenant décompressé côté Python → texte indexé pour recherche TF-IDF
- 7 nouveaux tests B5 dans tests/test_excalidraw.py (13/13 total)
- F2: excalidraw-viewer.test.mjs enregistré dans CI (lint job)
- F3: tests/e2e/excalidraw.spec.js (9 specs — ouverture .excalidraw/.excalidraw.md, création via modale/menu contextuel, toolbar, thème, pop-out, recherche)
- Fixtures test_vault/diagram.excalidraw + diagram.excalidraw.md

#67 Push notifications (Web Push API + VAPID)
- backend/push.py: router + VAPID keys + send_push_notification (pywebpush>=2.3.0)
- frontend/js/push.js: subscription UI + service worker integration
- tests/test_push.py: 10 tests (subscribe/list/unsubscribe/vapid key)
- requirements.txt + sw.js + locales push strings

#68 Health check enrichi
- backend/main.py: GET /api/health/detailed (admin) — memory/cpu/disk/backups/index/SSE connections
- backend/indexer.py: _last_full_index_ts tracking
- tests/conftest.py: admin_client fixture
- tests/test_api_main.py: 3 nouveaux tests health detailed

#77 Desktop jumplist
- desktop/src/jumplist.rs: Windows jumplist integration
- Cargo.toml/lock + capabilities + main.rs
- frontend/js/desktop.js: Tauri bridge (isTauriEnv, invoke, getSystemTheme, syncSystemTheme, shouldShowWizard, crash banner)
- tests/frontend/desktop.test.mjs: 21 tests JSDOM (détection, invoke degradation, theme gating, wizard, crash banner)
- tests/frontend/unit.test.mjs: desktop.js whitelisted (standalone global reader)

# Frontend & CI
- frontend/sw.js: réécriture complète (precache + push event handlers + offline)
- frontend/index.html: section push dans settings + about repositionné
- frontend/js/app.js: initDesktopIntegration + initPush
- CI .gitea/workflows/ci.yml: excalidraw-viewer.test.mjs ajouté au lint job

All checks: ruff clean, 552 backend tests pass, 9 frontend unit, 5 excalidraw-viewer, 21 desktop, 9 pane-manager, validate-imports 33 modules.
2026-09-09 23:18:29 -04:00

261 lines
10 KiB
Python

"""Tests for Excalidraw file support in ObsiGate."""
import json
EXCALIDRAW_SKELETON = {
"type": "excalidraw",
"version": 2,
"elements": [],
"appState": {"viewBackgroundColor": "#ffffff"},
"files": {},
}
EXCALIDRAW_WITH_CONTENT = {
"type": "excalidraw",
"version": 2,
"elements": [
{
"id": "rect-1",
"type": "rectangle",
"x": 100, "y": 200, "width": 300, "height": 150,
"angle": 0, "strokeColor": "#1e1e1e",
"backgroundColor": "#ffec99", "fillStyle": "solid",
"strokeWidth": 2, "strokeStyle": "solid",
"roughness": 1, "opacity": 100, "groupIds": [],
"roundness": {"type": 3},
"boundElements": None, "updated": 1,
"link": None, "locked": False,
},
{
"id": "text-1", "type": "text",
"x": 150, "y": 260, "width": 200, "height": 30,
"angle": 0, "strokeColor": "#1e1e1e",
"backgroundColor": "transparent", "fillStyle": "solid",
"strokeWidth": 2, "strokeStyle": "solid",
"roughness": 1, "opacity": 100, "groupIds": [],
"roundness": None, "boundElements": None,
"updated": 1, "link": None, "locked": False,
"text": "Hello Excalidraw", "fontSize": 20,
"fontFamily": 1, "textAlign": "center",
"verticalAlign": "middle", "containerId": "rect-1",
"originalText": "Hello Excalidraw",
"autoResize": True, "lineHeight": 1.25,
},
],
"appState": {"viewBackgroundColor": "#ffffff", "name": "My Diagram"},
"files": {},
}
# ████████████████████████████████████████████████████████████████████████
# B5 — Indexation du contenu texte Excalidraw (recherche TF-IDF)
# ████████████████████████████████████████████████████████████████████████
# Fixture lz-string "compressToBase64" générée par le vrai paquet
# [email protected] (JS) en 2026-09-09, validée byte-pour-byte contre le port
# Python de `_decompress_excalidraw`. Contient du texte accentué.
LZ_COMPRESSED_DIAGRAM = (
"N4IgLgngDgpiBcIYA8DGBDANgSwCYCd0B3EAGhADcZ8BnbAewDsEAmcmTGAWxkbBoQBtUHgQh0"
"ZcNDiIwKMJLnIFiABIdM9AAQBRNFjyES5evmwBzbIywAVeWPWZNu/TgLFJyBAEYADOQhWXwBf"
"UhFcMQAjRWkxJQVyeLEAJQBLmgBXLlStOVQAC0ZsAEcMmC0AMi1GejkBEzNLa0w7ZRT0rJy8"
"wpKyyuramHqQL3gAZn8QQPgAFhCwkFFEVBjYMXwYVDB0RnNOSUx0SI4xACF6ADu5LWwuKF"
"NtvjhyUYBWAIRX4IBdcnQoKAAZW2cgQoGsPDEABEhnRmKEQAAzbCcATwYDBYJAA"
)
_RAW_EXCALIDRAW_MD = """---
excalidraw-plugin: parsed
---
```compressed-json
{comp}
```
Ce texte hors bloc ne doit pas polluer l'index (on extrait la scène uniquement).
"""
def _make_excalidraw_md(compressed: str) -> str:
return _RAW_EXCALIDRAW_MD.format(comp=compressed)
class TestExcalidrawDetection:
"""Test that the /api/file endpoint correctly detects .excalidraw files."""
VAULT = "TestVault"
def test_valid_excalidraw_file(self, client):
"""A valid .excalidraw file should return is_excalidraw: true."""
path = "diagram.excalidraw"
resp = client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": json.dumps(EXCALIDRAW_WITH_CONTENT)},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/file/{self.VAULT}", params={"path": path})
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["is_excalidraw"] is True
assert "excalidraw_data" in data
assert len(data["excalidraw_data"]["elements"]) == 2
assert data["excalidraw_data"]["elements"][0]["type"] == "rectangle"
assert data["title"] == "My Diagram"
def test_empty_excalidraw_creation(self, client):
"""Creating a .excalidraw with empty content injects skeleton JSON."""
path = "empty.excalidraw"
resp = client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": ""},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/file/{self.VAULT}", params={"path": path})
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["is_excalidraw"] is True
assert data["excalidraw_data"]["elements"] == []
assert data["excalidraw_data"]["appState"]["viewBackgroundColor"] == "#ffffff"
def test_invalid_json_excalidraw_fallback(self, client):
"""A .excalidraw with invalid JSON should show as text, not Excalidraw."""
path = "corrupt.excalidraw"
resp = client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": "this is not valid json!!!"},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/file/{self.VAULT}", params={"path": path})
assert resp.status_code == 200, resp.text
data = resp.json()
assert not data.get("is_excalidraw")
def test_json_without_excalidraw_type(self, client):
"""A .excalidraw with valid JSON but wrong type falls through."""
path = "not-excalidraw.excalidraw"
resp = client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": '{"type":"something-else","data":[1,2,3]}'},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/file/{self.VAULT}", params={"path": path})
assert resp.status_code == 200, resp.text
data = resp.json()
assert not data.get("is_excalidraw")
def test_excalidraw_in_tree(self, client):
"""A .excalidraw file appears in the file tree."""
path = "tree-diagram.excalidraw"
resp = client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": json.dumps(EXCALIDRAW_SKELETON)},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/browse/{self.VAULT}", params={"path": ""})
assert resp.status_code == 200, resp.text
items = resp.json()["items"]
names = [i["name"] for i in items]
assert "tree-diagram.excalidraw" in names, f"Not found in: {names}"
def test_excalidraw_save_roundtrip(self, client):
"""Create, edit (PUT), and re-read an .excalidraw file."""
path = "roundtrip.excalidraw"
client.post(
f"/api/file/{self.VAULT}",
json={"path": path, "content": ""},
)
modified = dict(EXCALIDRAW_SKELETON)
modified["elements"] = [{
"id": "ellipse-1", "type": "ellipse",
"x": 50, "y": 50, "width": 100, "height": 80,
"angle": 0, "strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff", "fillStyle": "solid",
"strokeWidth": 2, "strokeStyle": "solid",
"roughness": 1, "opacity": 100, "groupIds": [],
"roundness": {"type": 2},
"boundElements": None, "updated": 1,
"link": None, "locked": False,
}]
resp = client.put(
f"/api/file/{self.VAULT}/save",
params={"path": path, "backup": "false"},
json={"content": json.dumps(modified)},
)
assert resp.status_code == 200, resp.text
resp = client.get(f"/api/file/{self.VAULT}", params={"path": path})
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["is_excalidraw"] is True
assert len(data["excalidraw_data"]["elements"]) == 1
assert data["excalidraw_data"]["elements"][0]["type"] == "ellipse"
class TestExcalidrawIndexableTextB5:
"""B5 — Indexation du texte des éléments (recherche TF-IDF)."""
def test_pure_json_extracts_element_text(self):
from backend.indexer import extract_excalidraw_indexable
out = extract_excalidraw_indexable(json.dumps(EXCALIDRAW_WITH_CONTENT))
assert "Hello Excalidraw" in out
def test_pure_json_skeleton_has_no_text(self):
from backend.indexer import extract_excalidraw_indexable
out = extract_excalidraw_indexable(json.dumps(EXCALIDRAW_SKELETON))
assert out.strip() == ""
def test_pure_json_binds_label_and_title(self):
from backend.indexer import extract_excalidraw_indexable
payload = {
"type": "excalidraw",
"version": 2,
"elements": [
{"id": "r", "type": "rectangle", "label": "Boite bleue"},
{"id": "t", "type": "text", "text": "texte simple",
"originalText": "texte simple"},
],
"appState": {},
"files": {},
}
out = extract_excalidraw_indexable(json.dumps(payload))
assert "Boite bleue" in out
assert "texte simple" in out
def test_lz_decompress_matches_js_fixture(self):
"""Le port Python décode un payload lz-string produit par le vrai JS."""
from backend.indexer import _decompress_excalidraw
data = _decompress_excalidraw(LZ_COMPRESSED_DIAGRAM)
assert data is not None
assert data["type"] == "excalidraw"
elements = data["elements"]
texts = {e.get("text") or e.get("label") for e in elements if isinstance(e, dict)}
assert "Hello Excalidraw" in texts
assert "Résumé technique & notes" in texts
assert "Boîte importante" in texts
def test_lz_decompress_rejects_garbage(self):
from backend.indexer import _decompress_excalidraw
assert _decompress_excalidraw("") is None
assert _decompress_excalidraw("@@@not-lz@@@") is None
assert _decompress_excalidraw("!!!") is None
def test_excalidraw_md_compressed_extracts_text(self):
""".excalidraw.md (format plugin Obsidian) → texte indexé."""
from backend.indexer import extract_excalidraw_indexable
out = extract_excalidraw_indexable(_make_excalidraw_md(LZ_COMPRESSED_DIAGRAM))
assert "Hello Excalidraw" in out
assert "Boîte importante" in out
def test_excalidraw_md_missing_block_returns_empty(self):
from backend.indexer import extract_excalidraw_indexable
out = extract_excalidraw_indexable("---\nexcalidraw-plugin: parsed\n---\n(pas de bloc)")
assert out.strip() == ""