feat: styles minimaux, fusions et volets figes dans la visionneuse xlsx #153
A15 — le rendu tableur lit les metadonnees du classeur (une charge en mode
normal par feuille) et les restitue : styles inline data-driven (couleur de
police en premier, fond, gras/italique, format numerique signale en mono),
alignements non-defaut, plages fusionnees (rowSpan/colSpan cote client) et
ancrage des volets figes (sticky rows/cols). Le chargement lazy replie les
metadonnees de chaque fenetre. Contrat API : styles/aligns = {ref: css},
merges = [ranges], freeze = ancre. 9 tests backend + contre-preuve (5 echecs
sur neutralisation), JSDOM 33/33.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Style metadata in the .xlsx viewer (#153 A15): bold, colors, number formats,
|
||||
merged ranges and freeze panes — plus the endpoint wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
openpyxl = pytest.importorskip("openpyxl")
|
||||
|
||||
VAULT = "TestVault"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def styled_book(test_vault_dir: str) -> str:
|
||||
"""A workbook with a bold red-font header, a filled cell, a merge and a freeze."""
|
||||
from openpyxl.styles import Font, PatternFill
|
||||
|
||||
path = Path(test_vault_dir) / "styles.xlsx"
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Data"
|
||||
ws["A1"] = "En-tête"
|
||||
ws["A1"].font = Font(bold=True, italic=True, color="FFCC0000")
|
||||
ws["B1"] = "Total"
|
||||
ws["B1"].fill = PatternFill("solid", fgColor="FFFFEE99")
|
||||
ws["B2"] = 1234.5
|
||||
ws["B2"].number_format = "#,##0.00"
|
||||
ws["C3"] = "fusionnée"
|
||||
ws.merge_cells("C3:D3")
|
||||
ws["C3"].value = "fusionnée"
|
||||
ws.freeze_panes = "A2"
|
||||
wb.save(path)
|
||||
return str(path)
|
||||
|
||||
|
||||
class TestStyleReading:
|
||||
def test_bold_italic_and_font_color(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
styles = read_sheet_styles(Path(styled_book), "Data")
|
||||
a1 = styles["A1"]
|
||||
assert "font-weight:600" in a1["style"]
|
||||
assert "font-style:italic" in a1["style"]
|
||||
assert a1["style"].startswith("color:#cc0000")
|
||||
|
||||
def test_background_fill_is_read(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
styles = read_sheet_styles(Path(styled_book), "Data")
|
||||
assert "background:#ffee99" in styles["B1"]["style"]
|
||||
|
||||
def test_number_format_is_signalled(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
styles = read_sheet_styles(Path(styled_book), "Data")
|
||||
assert "font-family" in styles["B2"]["style"]
|
||||
|
||||
def test_plain_cell_has_no_entry(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
styles = read_sheet_styles(Path(styled_book), "Data")
|
||||
assert "A2" not in styles # untouched cell
|
||||
|
||||
def test_merges_and_freeze(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_freeze, read_sheet_merges
|
||||
|
||||
assert read_sheet_merges(Path(styled_book), "Data") == ["C3:D3"]
|
||||
assert read_sheet_freeze(Path(styled_book), "Data") == "A2"
|
||||
|
||||
def test_unknown_sheet_yields_empty(self, styled_book):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
assert read_sheet_styles(Path(styled_book), "Nope") == {}
|
||||
|
||||
def test_broken_file_yields_empty(self, test_vault_dir):
|
||||
from backend.xlsx_reader import read_sheet_styles
|
||||
|
||||
bad = Path(test_vault_dir) / "broken-styles.xlsx"
|
||||
bad.write_bytes(b"not a zip")
|
||||
assert read_sheet_styles(bad, "Data") == {}
|
||||
|
||||
|
||||
class TestEndpointWiring:
|
||||
def test_read_response_carries_styles_merges_freeze(self, client, styled_book):
|
||||
resp = client.get(f"/api/file/{VAULT}", params={"path": "styles.xlsx"})
|
||||
assert resp.status_code == 200
|
||||
sheet = resp.json()["xlsx_sheets"][0]
|
||||
assert sheet["styles"]["A1"].startswith("color:#cc0000")
|
||||
assert sheet["aligns"] == {} or isinstance(sheet["aligns"], dict)
|
||||
assert sheet["merges"] == ["C3:D3"]
|
||||
assert sheet["freeze"] == "A2"
|
||||
|
||||
def test_rendered_html_carries_the_inline_style(self, client, styled_book):
|
||||
resp = client.get(f"/api/file/{VAULT}", params={"path": "styles.xlsx"})
|
||||
html = resp.json()["xlsx_sheets"][0]["html"]
|
||||
assert 'data-cell="A1" style="' in html
|
||||
assert "font-weight:600" in html
|
||||
Reference in New Issue
Block a user