feat: completude de l'editeur Excel - mise en forme, sortie, concurrence, cache, outils IA #156
L'editeur lisait les styles mais ne les ecrivait pas, exportait la feuille entiere et laissait le dernier-ecrivain gagner entre processus. - A8 : bouton Mise en forme (gras/italique/souligne, alignements, couleurs via selecteur natif, formats de nombre, fusion, volets figes, largeur/hauteur) et nouvelle route PUT .../xlsx/style (verrou, backup, swap atomique, garde de perte, If-Match) ; A9 : decision "pas de moteur de formule" annoncee dans l'UI ; A10 : undo/redo unifie, piles par fichier conservees au re-rendu - A11 : export de la selection + Markdown/HTML/impression et recherche sur toutes les feuilles ; A12 : concurrence optimiste (If-Match -> 409 reparable, retry qui relit) ; A13 : cache des metadonnees par (chemin, mtime, taille) - A14 : outils IA .xlsm/.csv + search_workbook, analyze_range, edit_xlsx_structure - tests : test_xlsx_styles.py (17), test_spreadsheet_tools.py (34), TestOptimisticConcurrency/TestMetaCache, JSDOM xlsx-viewer 107/107
This commit is contained in:
@@ -95,9 +95,32 @@ class TestXlsmEditable:
|
||||
with zipfile.ZipFile(_vault_file(client, xlsm_book)) as zf:
|
||||
assert "xl/vbaProject.bin" in zf.namelist()
|
||||
|
||||
def test_lossy_gate_is_skipped_for_xlsm(self, formats_vault, xlsm_book):
|
||||
def test_sheet_window_is_served_for_xlsm(self, client, xlsm_book):
|
||||
"""BUG-097 — a .xlsm is editable, so its lazy row windows must work."""
|
||||
resp = client.get(
|
||||
f"/api/file/{VAULT}/xlsx/sheet",
|
||||
params={"path": xlsm_book, "sheet": "Data", "offset": 0, "limit": 100},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["sheet"] == "Data"
|
||||
assert 'data-cell="A2"' in body["html"]
|
||||
assert body["has_more"] is False
|
||||
|
||||
def test_sheet_window_still_refuses_other_formats(self, client, formats_vault):
|
||||
(formats_vault / "plain.csv").write_text("a,b\n", encoding="utf-8")
|
||||
resp = client.get(
|
||||
f"/api/file/{VAULT}/xlsx/sheet",
|
||||
params={"path": "plain.csv", "sheet": "Feuille1"},
|
||||
)
|
||||
assert resp.status_code == 415
|
||||
|
||||
def test_lossy_gate_is_skipped_for_xlsm(self, client, formats_vault, xlsm_book):
|
||||
from backend.services.mutations import edit_xlsx_cells
|
||||
|
||||
# `client` is required: the service resolves the vault through the app
|
||||
# registry (the test used to pass only thanks to state leaked by an
|
||||
# earlier test in this class).
|
||||
# No force flag: the save must succeed despite the vbaProject part
|
||||
# (it would raise xlsx_lossy_content on a plain .xlsx).
|
||||
result = edit_xlsx_cells(VAULT, xlsm_book, "Data", {"A2": "Alan"})
|
||||
@@ -149,6 +172,22 @@ class TestLegacyReadOnly:
|
||||
assert sheet["rows"] == 0
|
||||
|
||||
|
||||
class TestCsvDelimiter:
|
||||
"""BUG-098 — the delimiter is detected, not assumed to be a comma."""
|
||||
|
||||
def test_sniffing_handles_the_common_separators(self):
|
||||
from backend.xlsx_reader import sniff_csv_delimiter
|
||||
|
||||
assert sniff_csv_delimiter("a;b\n1;2\n") == ";"
|
||||
assert sniff_csv_delimiter("a,b\n1,2\n") == ","
|
||||
assert sniff_csv_delimiter("a\tb\n1\t2\n") == "\t"
|
||||
|
||||
def test_single_column_falls_back_to_a_comma(self):
|
||||
from backend.xlsx_reader import sniff_csv_delimiter
|
||||
|
||||
assert sniff_csv_delimiter("juste\nune\ncolonne\n") == ","
|
||||
|
||||
|
||||
class TestCsvEditable:
|
||||
def test_read_renders_the_xlsx_shaped_table(self, client, formats_vault):
|
||||
(formats_vault / "liste.csv").write_text("a,b\n1,2\n", encoding="utf-8")
|
||||
@@ -197,6 +236,42 @@ class TestCsvEditable:
|
||||
text = _vault_file(client, "quotes.csv").read_text(encoding="utf-8")
|
||||
assert '"Martin, Pierre"' in text
|
||||
|
||||
def test_semicolon_csv_renders_multiple_columns(self, client, formats_vault):
|
||||
"""BUG-098 — a French (semicolon) CSV is no longer a single column."""
|
||||
(formats_vault / "fr.csv").write_text("Nom;Ville\nAda;Paris\n", encoding="utf-8")
|
||||
html = client.get(
|
||||
f"/api/file/{VAULT}", params={"path": "fr.csv"}
|
||||
).json()["html"]
|
||||
assert 'data-cell="B1"' in html and 'data-cell="B2"' in html
|
||||
assert "Paris" in html
|
||||
|
||||
def test_semicolon_is_preserved_when_saving(self, client, formats_vault):
|
||||
"""BUG-098 — the write-back keeps the file's own delimiter."""
|
||||
(formats_vault / "fr.csv").write_text("Nom;Ville\nAda;Paris\n", encoding="utf-8")
|
||||
resp = client.put(
|
||||
f"/api/file/{VAULT}/csv/save",
|
||||
params={"path": "fr.csv"},
|
||||
json={"cells": {"B2": "Lyon"}},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
text = _vault_file(client, "fr.csv").read_text(encoding="utf-8")
|
||||
assert "Nom;Ville" in text
|
||||
assert "Ada;Lyon" in text
|
||||
assert "," not in text # the source delimiter is not switched to a comma
|
||||
|
||||
def test_quoted_semicolon_survives_a_round_trip(self, client, formats_vault):
|
||||
(formats_vault / "quote-fr.csv").write_text(
|
||||
'Nom;Note\n"Dupont; Jean";ok\n', encoding="utf-8"
|
||||
)
|
||||
client.put(
|
||||
f"/api/file/{VAULT}/csv/save",
|
||||
params={"path": "quote-fr.csv"},
|
||||
json={"cells": {"B2": "modifié"}},
|
||||
)
|
||||
text = _vault_file(client, "quote-fr.csv").read_text(encoding="utf-8")
|
||||
assert '"Dupont; Jean"' in text
|
||||
assert ";modifié" in text
|
||||
|
||||
def test_bad_reference_is_refused(self, client, formats_vault):
|
||||
(formats_vault / "liste.csv").write_text("a\n", encoding="utf-8")
|
||||
resp = client.put(
|
||||
|
||||
Reference in New Issue
Block a user