A worksheet freezes the top row so row 2 becomes the top-left visible cell on scroll, declared via a <pane> child of <sheetView>.
Library verdicts: python-xlsx: — xlsxjs: pass
| Feature id | xlsx/freeze-panes |
|---|---|
| Format | xlsx |
| Category | sheet-layout |
| Spec | ecma-376-5-part-1 § 18.3.1.68 sheetView |
| ID / part | Predicate | XPath | python-xlsx | xlsxjs |
|---|---|---|---|---|
pane-state-frozenxl/worksheets/sheet1.xml | equal = frozenThe <pane>@state must equal 'frozen'. | string(//x:sheetViews/x:sheetView/x:pane/@state) | — | — |
pane-topleft-a2xl/worksheets/sheet1.xml | equal = A2The <pane>@topLeftCell must equal 'A2' (the first non-frozen cell). | string(//x:sheetViews/x:sheetView/x:pane/@topLeftCell) | — | — |
pane-ysplit-1xl/worksheets/sheet1.xml | equal = 1The <pane>@ySplit must equal '1' (one row frozen above the split). | string(//x:sheetViews/x:sheetView/x:pane/@ySplit) | — | — |
| ID | Predicate | Selector | python-xlsx | xlsxjs |
|---|---|---|---|---|
freeze-panes-header-text | css_selector/ match-text = HeaderThe rendered sheet must contain a cell with the fixture header text. | section.xlsx td, section.xlsx th | — | pass |
freeze-panes-marker | css_selector/ existThe renderer must mark the frozen row via a class (xlsx-frozen-row / any class containing 'frozen') or a data-frozen attribute. xlsxjs emits tr.xlsx-frozen-row for frozen rows per its CLAUDE.md. | section.xlsx tr.xlsx-frozen-row, section.xlsx [data-frozen], section.xlsx [class*='frozen'] | — | pass |
sheet-count | css_selector/ equal-countExactly one rendered sheet section. Catches renderers that emit zero sheets (hard failure) or duplicate a sheet. | section.xlsx | — | — |
scripts/gen_freeze_panes.py
#!/usr/bin/env python3
"""Generate ``fixtures/xlsx/freeze-panes.xlsx``.
A minimal one-sheet workbook with row 1 frozen so that row 2 is the
top-left visible cell when the sheet is scrolled. Designed to satisfy
the assertions in ``features/xlsx/freeze-panes.json``.
"""
from __future__ import annotations
from pathlib import Path
from xlsx import Workbook
import datetime as _dt
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "xlsx" / "freeze-panes.xlsx"
def main() -> int:
wb = Workbook()
ws = wb.active
ws["A1"] = "Header"
ws["A2"] = "Data"
ws.freeze_panes = "A2"
_OUT.parent.mkdir(parents=True, exist_ok=True)
wb.save(_OUT, zip_date_time=_REPRODUCIBLE_DT)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "xlsx/freeze-panes",
"title": "Freeze panes (top row frozen)",
"format": "xlsx",
"category": "sheet-layout",
"summary": "A worksheet freezes the top row so row 2 becomes the top-left visible cell on scroll, declared via a <pane> child of <sheetView>.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "18.3.1.68",
"element": "sheetView",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/SpreadsheetML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/sml.xsd",
"notes": "Frozen panes in SpreadsheetML live on a <pane> child of <sheetView> inside <sheetViews>. @state='frozen' distinguishes freeze from split; @ySplit='N' says N rows are frozen above (@xSplit would do the analogous for columns); @topLeftCell names the first non-frozen cell that becomes the scroll origin. Setting ws.freeze_panes='A2' in python-xlsx produces ySplit=1 with topLeftCell=A2. SpreadsheetML parts use a default namespace with no prefix; XPath therefore needs an explicit binding (here 'x')."
},
"fixtures": {
"machine": "xlsx/freeze-panes",
"office": "xlsx/freeze-panes"
},
"generator": {
"python": "scripts/gen_freeze_panes.py"
},
"assertions": [
{
"id": "pane-state-frozen",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "string(//x:sheetViews/x:sheetView/x:pane/@state)",
"must": "equal",
"value": "frozen",
"description": "The <pane>@state must equal 'frozen'."
},
{
"id": "pane-topleft-a2",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "string(//x:sheetViews/x:sheetView/x:pane/@topLeftCell)",
"must": "equal",
"value": "A2",
"description": "The <pane>@topLeftCell must equal 'A2' (the first non-frozen cell)."
},
{
"id": "pane-ysplit-1",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "string(//x:sheetViews/x:sheetView/x:pane/@ySplit)",
"must": "equal",
"value": "1",
"description": "The <pane>@ySplit must equal '1' (one row frozen above the split)."
}
],
"render_assertions": [
{
"id": "freeze-panes-header-text",
"kind": "css_selector",
"selector": "section.xlsx td, section.xlsx th",
"must": "match-text",
"value": "Header",
"description": "The rendered sheet must contain a cell with the fixture header text."
},
{
"id": "freeze-panes-marker",
"kind": "css_selector",
"selector": "section.xlsx tr.xlsx-frozen-row, section.xlsx [data-frozen], section.xlsx [class*='frozen']",
"must": "exist",
"description": "The renderer must mark the frozen row via a class (xlsx-frozen-row / any class containing 'frozen') or a data-frozen attribute. xlsxjs emits tr.xlsx-frozen-row for frozen rows per its CLAUDE.md."
},
{
"id": "sheet-count",
"kind": "css_selector",
"selector": "section.xlsx",
"must": "equal-count",
"count": 1,
"description": "Exactly one rendered sheet section. Catches renderers that emit zero sheets (hard failure) or duplicate a sheet."
}
]
}
