feat: éditeur tableur — grille A → Z par blocs, barre de menus Sheets, panneau de couleurs, création .xlsx (#180 → #186)
Restaure et termine le lot tableur resté non committé. Il n'existait que dans un `git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js, color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le stash, créé avant le commit BUG-108, n'a jamais été restauré. #180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint), grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu Fichier au niveau des onglets, barre épinglée pleine largeur. #181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans quitter contenteditable, double-clic / F2 / première frappe qui remplace, Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts. #182 Désélection fiable (clic simple sans dépendre du focus), contours de plage non empilés (sélecteur de classes sans point), couleurs texte/fond sur une plage, clic droit qui préserve la sélection multiple. #183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD, PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était rejetée par normHex). #184 Peinture de format complète (toutes propriétés, source sans format = réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de cellules `ref → td`, court-circuits formule/styles, marqueur data-wired. #185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36 annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème. #186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC (openpyxl) au lieu d'une charge utile texte illisible. Corrections trouvées en restaurant et en exerçant le lot : - fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ; - sorties Markdown / HTML / Imprimer de la barre de menus inertes (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ; - collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ; - curseur col/row-resize absent quand le pointeur est sur la table elle-même ; - l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown, HTML, impression) ; - clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au seul appui long tactile) ; - une entrée indisponible laissait la barre de menus ouverte. Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ; 33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19, xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer 19/19.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# tests/test_api_main.py — Integration tests for main.py API endpoints
|
||||
# Uses the existing app_with_vault / client fixtures from conftest.py
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# Health & Metadata
|
||||
@@ -200,6 +203,45 @@ class TestFileCRUD:
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
|
||||
def test_create_xlsx_empty_workbook(self, client, test_vault_dir):
|
||||
"""#186 - "Create a file" > Excel must produce a readable workbook.
|
||||
|
||||
An .xlsx is a ZIP archive, not a text file: writing the empty payload
|
||||
of the modal as text would leave a file the viewer cannot open.
|
||||
"""
|
||||
openpyxl = pytest.importorskip("openpyxl")
|
||||
|
||||
resp = client.post("/api/file/TestVault", json={
|
||||
"path": "nouveau.xlsx",
|
||||
"content": "",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["success"] is True
|
||||
|
||||
# The archive is a real workbook: one empty sheet, loadable by openpyxl.
|
||||
wb = openpyxl.load_workbook(Path(test_vault_dir) / "nouveau.xlsx")
|
||||
assert wb.sheetnames == ["Feuille1"]
|
||||
|
||||
# And the viewer serves it as a spreadsheet.
|
||||
resp2 = client.get("/api/file/TestVault", params={"path": "nouveau.xlsx"})
|
||||
assert resp2.status_code == 200
|
||||
body = resp2.json()
|
||||
assert body["is_xlsx"] is True
|
||||
assert [s["name"] for s in body["xlsx_sheets"]] == ["Feuille1"]
|
||||
|
||||
def test_create_xlsx_in_new_subdir(self, client):
|
||||
"""#186 - same in a subdirectory that does not exist yet."""
|
||||
pytest.importorskip("openpyxl")
|
||||
|
||||
resp = client.post("/api/file/TestVault", json={
|
||||
"path": "Tableaux/annee.xlsx",
|
||||
"content": "",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
resp2 = client.get("/api/file/TestVault", params={"path": "Tableaux/annee.xlsx"})
|
||||
assert resp2.status_code == 200
|
||||
assert resp2.json()["is_xlsx"] is True
|
||||
|
||||
def test_save_file(self, client):
|
||||
"""Save (update) an existing file"""
|
||||
resp = client.put("/api/file/TestVault/save?path=note1.md", json={
|
||||
|
||||
Reference in New Issue
Block a user