feat: split view multi-panneaux (#75) — PaneManager intégré avec split right/down, menu contextuel, raccourcis clavier, CSS responsive et tests
CI / lint (push) Failing after 35s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 15s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
CI / lint (push) Failing after 35s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / e2e (push) Has been skipped
CI / security (push) Successful in 15s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled
- PaneManager: splitRight/splitDown avec 1-4 panneaux, persistance localStorage, resize - Viewer/TabManager pane-aware: fonctions getContentArea/getTabBar/getTabList - Menu contextuel onglets: actions Diviser à droite / Diviser en bas / Fermer le panneau - Raccourcis clavier: Ctrl+Alt+\ (split right), Ctrl+Alt+Shift+\ (split down), Ctrl+Alt+W (fermer panneau) - CSS pane-grid: grille, poignées resize, bordure active pane, responsive (désactivé <768px) - Tests: 22 nouveaux tests unitaires + régression (307 total, 100% green) - Déployé sur Docker test (localhost:2021)
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Tests for the Multi-Pane Split View (#75).
|
||||
|
||||
Since PaneManager is a frontend JS module, these tests verify:
|
||||
1. Backend integration: all file/sidebar/vault APIs still work (no regression)
|
||||
2. Static file serving: pane-manager.js is served correctly
|
||||
3. CSS: style.css includes pane grid classes
|
||||
4. HTML: index.html includes the content-wrapper structure needed for panes
|
||||
"""
|
||||
import pytest
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestPaneManagerStaticAssets:
|
||||
"""Verify pane-manager.js, style.css, and index.html are properly structured."""
|
||||
|
||||
def test_pane_manager_js_exists(self):
|
||||
"""PaneManager module file is present."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
assert path.exists(), f"pane-manager.js not found at {path}"
|
||||
|
||||
def test_pane_manager_js_exports(self):
|
||||
"""PaneManager exports the expected API."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "export default PaneManager" in content
|
||||
assert "window.PaneManager = PaneManager" in content
|
||||
|
||||
def test_pane_manager_has_split_methods(self):
|
||||
"""PaneManager has splitRight and splitDown methods."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "splitRight()" in content
|
||||
assert "splitDown()" in content
|
||||
assert "closePane(" in content
|
||||
assert "closeActivePane()" in content
|
||||
assert "isSplit()" in content
|
||||
assert "getActiveContentArea()" in content
|
||||
|
||||
def test_pane_manager_has_keyboard_shortcuts(self):
|
||||
"""Keyboard shortcuts are registered."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
assert "ctrlKey" in content
|
||||
assert '"\\\\"' in content or "'\\\\'" in content # backslash key
|
||||
|
||||
def test_style_css_has_pane_grid_classes(self):
|
||||
"""style.css includes pane grid CSS classes."""
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert ".pane-grid" in content
|
||||
assert ".pane-container" in content
|
||||
assert ".pane-resize-handle" in content
|
||||
assert ".pane-active" in content
|
||||
|
||||
def test_style_css_has_responsive_split(self):
|
||||
"""Responsive rules disable split below 768px."""
|
||||
path = PROJECT_ROOT / "frontend" / "style.css"
|
||||
content = path.read_text()
|
||||
assert "@media (max-width: 768px)" in content
|
||||
assert ".pane-grid" in content
|
||||
|
||||
def test_viewer_uses_pane_aware_helper(self):
|
||||
"""viewer.js has getContentArea() helper and uses it."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "viewer.js"
|
||||
content = path.read_text()
|
||||
assert "function getContentArea()" in content
|
||||
assert "window.PaneManager" in content
|
||||
# Should NOT have direct document.getElementById("content-area") except in fallback
|
||||
direct_refs = [l for l in content.split("\n")
|
||||
if 'document.getElementById("content-area")' in l
|
||||
and "fallback" not in l.lower()]
|
||||
# Only the fallback inside getContentArea should remain
|
||||
assert len(direct_refs) <= 2, f"Too many direct content-area refs: {direct_refs}"
|
||||
|
||||
def test_ui_tab_manager_has_pane_helpers(self):
|
||||
"""TabManager in ui.js has _getContentArea, _getTabBar, _getTabList."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
assert "_getContentArea()" in content
|
||||
assert "_getTabBar()" in content
|
||||
assert "_getTabList()" in content
|
||||
assert "activatePaneTab(" in content
|
||||
|
||||
def test_ui_context_menu_has_split_actions(self):
|
||||
"""Tab context menu includes 'Diviser à droite' and 'Diviser en bas'."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
assert "Diviser à droite" in content
|
||||
assert "Diviser en bas" in content
|
||||
assert "Fermer le panneau" in content
|
||||
assert 'data-action="splitRight"' in content
|
||||
assert 'data-action="splitDown"' in content
|
||||
|
||||
def test_html_has_content_wrapper(self):
|
||||
"""index.html has the content-wrapper structure needed for panes."""
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
assert 'class="content-wrapper"' in content
|
||||
assert 'id="content-area"' in content
|
||||
assert 'id="tab-bar"' in content
|
||||
assert 'id="tab-list"' in content
|
||||
|
||||
def test_html_uses_es_modules(self):
|
||||
"""index.html uses ES module scripts for the frontend modules."""
|
||||
path = PROJECT_ROOT / "frontend" / "index.html"
|
||||
content = path.read_text()
|
||||
# Should load the ES module app.js orchestrator
|
||||
assert 'type="module"' in content
|
||||
|
||||
def test_brace_balance_pane_manager(self):
|
||||
"""PaneManager JS has balanced braces, parens, brackets."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "pane-manager.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
|
||||
def test_brace_balance_viewer(self):
|
||||
"""viewer.js has balanced braces, parens, brackets."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "viewer.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
|
||||
def test_brace_balance_ui(self):
|
||||
"""ui.js has balanced braces, parens, brackets."""
|
||||
path = PROJECT_ROOT / "frontend" / "js" / "ui.js"
|
||||
content = path.read_text()
|
||||
for char, name in [("{", "braces"), ("(", "parens"), ("[", "brackets")]:
|
||||
close_map = {"{": "}", "(": ")", "[": "]"}
|
||||
assert content.count(char) == content.count(close_map[char]), \
|
||||
f"Unbalanced {name}: {content.count(char)} opens vs {content.count(close_map[char])} closes"
|
||||
|
||||
|
||||
class TestBackendRegression:
|
||||
"""Verify backend APIs still work with frontend changes."""
|
||||
|
||||
def test_file_view_endpoint(self, client):
|
||||
"""GET /api/file/{vault} still returns expected fields."""
|
||||
resp = client.get("/api/file/TestVault", params={"path": "note1.md"})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "title" in data or "path" in data
|
||||
|
||||
def test_vault_list_endpoint(self, client):
|
||||
"""GET /api/vaults still works."""
|
||||
resp = client.get("/api/vaults")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
def test_search_endpoint(self, client):
|
||||
"""GET /api/search still works."""
|
||||
resp = client.get("/api/search?q=test")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "results" in data
|
||||
|
||||
def test_sidebar_tree_endpoint(self, client):
|
||||
"""GET /api/sidebar/tree/{vault} still works."""
|
||||
resp = client.get("/api/sidebar/tree/vault1")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_static_css_served(self, client):
|
||||
"""style.css is served correctly."""
|
||||
resp = client.get("/static/style.css")
|
||||
assert resp.status_code == 200
|
||||
assert "text/css" in resp.headers.get("content-type", "")
|
||||
|
||||
def test_static_js_served(self, client):
|
||||
"""app.js (ES module) is served correctly."""
|
||||
resp = client.get("/static/js/app.js")
|
||||
assert resp.status_code == 200
|
||||
assert "javascript" in resp.headers.get("content-type", "").lower()
|
||||
|
||||
def test_pane_manager_js_served(self, client):
|
||||
"""pane-manager.js is served as static asset."""
|
||||
resp = client.get("/static/js/pane-manager.js")
|
||||
assert resp.status_code == 200
|
||||
assert "javascript" in resp.headers.get("content-type", "").lower()
|
||||
assert "PaneManager" in resp.text
|
||||
|
||||
def test_index_html_served(self, client):
|
||||
"""index.html is served and contains content-wrapper."""
|
||||
resp = client.get("/")
|
||||
# May redirect or return 200 depending on auth
|
||||
assert resp.status_code in (200, 302, 401)
|
||||
Reference in New Issue
Block a user