@lotics/xlsx 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { exportWorkbook } from "./xlsx_writer";
|
|
3
|
+
import { parseExcelBuffer, parseExcelFromZip } from "./excel_parser";
|
|
4
|
+
import { unzipXlsx } from "./ooxml_zip";
|
|
5
|
+
import { WorkbookModel, SheetModel, StyleRegistry } from "./workbook_model";
|
|
6
|
+
|
|
7
|
+
function createTestWorkbook(): WorkbookModel {
|
|
8
|
+
const styles = new StyleRegistry();
|
|
9
|
+
const sheet = new SheetModel("TestSheet", styles);
|
|
10
|
+
|
|
11
|
+
// String, number, boolean values
|
|
12
|
+
sheet.set("A1", "hello");
|
|
13
|
+
sheet.set("B1", 42);
|
|
14
|
+
sheet.set("C1", true);
|
|
15
|
+
|
|
16
|
+
// Bold style
|
|
17
|
+
const boldIdx = styles.intern({ fontBold: true });
|
|
18
|
+
sheet.set("A2", "bold", styles.get(boldIdx));
|
|
19
|
+
|
|
20
|
+
// Color style
|
|
21
|
+
const colorIdx = styles.intern({ fontColor: "#FF0000", backgroundColor: "#00FF00" });
|
|
22
|
+
sheet.set("A3", "colored", styles.get(colorIdx));
|
|
23
|
+
|
|
24
|
+
// Merged cells
|
|
25
|
+
sheet.mergedCells.push("D1:E2");
|
|
26
|
+
|
|
27
|
+
// Freeze pane
|
|
28
|
+
sheet.freeze = { row: 1, col: 0 };
|
|
29
|
+
|
|
30
|
+
// Second sheet
|
|
31
|
+
const sheet2 = new SheetModel("Sheet2");
|
|
32
|
+
sheet2.set("A1", "other");
|
|
33
|
+
|
|
34
|
+
return new WorkbookModel({ sheets: [sheet, sheet2], activeSheetIndex: 0, styles });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("xlsx round-trip: exportWorkbook -> parseExcelBuffer", () => {
|
|
38
|
+
const wb = createTestWorkbook();
|
|
39
|
+
const xlsx = exportWorkbook(wb);
|
|
40
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
41
|
+
|
|
42
|
+
it("preserves sheet count and names", () => {
|
|
43
|
+
expect(parsed.sheets).toHaveLength(2);
|
|
44
|
+
expect(parsed.sheets[0].name).toBe("TestSheet");
|
|
45
|
+
expect(parsed.sheets[1].name).toBe("Sheet2");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("preserves string cell value", () => {
|
|
49
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
50
|
+
expect(row1).toBeDefined();
|
|
51
|
+
const cellA1 = row1!.cells.find((c) => c.column === 1);
|
|
52
|
+
expect(cellA1).toBeDefined();
|
|
53
|
+
expect(cellA1!.value).toBe("hello");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("preserves number cell value", () => {
|
|
57
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
58
|
+
const cellB1 = row1!.cells.find((c) => c.column === 2);
|
|
59
|
+
expect(cellB1).toBeDefined();
|
|
60
|
+
expect(cellB1!.value).toBe("42");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves boolean cell value", () => {
|
|
64
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
65
|
+
const cellC1 = row1!.cells.find((c) => c.column === 3);
|
|
66
|
+
expect(cellC1).toBeDefined();
|
|
67
|
+
expect(cellC1!.value).toBe("TRUE");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("preserves bold style", () => {
|
|
71
|
+
const row2 = parsed.sheets[0].rows.find((r) => r.index === 2);
|
|
72
|
+
const cellA2 = row2!.cells.find((c) => c.column === 1);
|
|
73
|
+
expect(cellA2).toBeDefined();
|
|
74
|
+
expect(cellA2!.style.fontBold).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("preserves font and background color", () => {
|
|
78
|
+
const row3 = parsed.sheets[0].rows.find((r) => r.index === 3);
|
|
79
|
+
const cellA3 = row3!.cells.find((c) => c.column === 1);
|
|
80
|
+
expect(cellA3).toBeDefined();
|
|
81
|
+
expect(cellA3!.style.fontColor).toMatch(/FF0000|rgb\(255,\s*0,\s*0\)/);
|
|
82
|
+
expect(cellA3!.style.backgroundColor).toMatch(/00FF00|rgb\(0,\s*255,\s*0\)/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("preserves merged cells", () => {
|
|
86
|
+
const merges = parsed.sheets[0].mergedCells;
|
|
87
|
+
expect(merges.length).toBeGreaterThanOrEqual(1);
|
|
88
|
+
const merge = merges.find(
|
|
89
|
+
(m) => m.startCol === 4 && m.startRow === 1 && m.endCol === 5 && m.endRow === 2,
|
|
90
|
+
);
|
|
91
|
+
expect(merge).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("preserves freeze pane", () => {
|
|
95
|
+
expect(parsed.sheets[0].freezePane).toBeDefined();
|
|
96
|
+
expect(parsed.sheets[0].freezePane!.frozenRows).toBe(1);
|
|
97
|
+
expect(parsed.sheets[0].freezePane!.frozenCols).toBe(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("preserves second sheet content", () => {
|
|
101
|
+
const row1 = parsed.sheets[1].rows.find((r) => r.index === 1);
|
|
102
|
+
expect(row1).toBeDefined();
|
|
103
|
+
const cellA1 = row1!.cells.find((c) => c.column === 1);
|
|
104
|
+
expect(cellA1).toBeDefined();
|
|
105
|
+
expect(cellA1!.value).toBe("other");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("border serialization round-trip", () => {
|
|
110
|
+
const styles = new StyleRegistry();
|
|
111
|
+
const sheet = new SheetModel("Borders", styles);
|
|
112
|
+
|
|
113
|
+
const thinBorderIdx = styles.intern({
|
|
114
|
+
borderTop: { width: 1, style: "thin", color: "#000000" },
|
|
115
|
+
borderBottom: { width: 1, style: "thin", color: "#000000" },
|
|
116
|
+
borderLeft: { width: 1, style: "thin", color: "#000000" },
|
|
117
|
+
borderRight: { width: 1, style: "thin", color: "#000000" },
|
|
118
|
+
});
|
|
119
|
+
sheet.set("A1", "bordered", styles.get(thinBorderIdx));
|
|
120
|
+
|
|
121
|
+
const thickTopIdx = styles.intern({
|
|
122
|
+
borderTop: { width: 2, style: "thick", color: "#FF0000" },
|
|
123
|
+
});
|
|
124
|
+
sheet.set("B1", "thick top", styles.get(thickTopIdx));
|
|
125
|
+
|
|
126
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
127
|
+
const xlsx = exportWorkbook(wb);
|
|
128
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
129
|
+
|
|
130
|
+
it("preserves thin borders on all sides", () => {
|
|
131
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
132
|
+
const cellA1 = row1!.cells.find((c) => c.column === 1);
|
|
133
|
+
expect(cellA1).toBeDefined();
|
|
134
|
+
// Parser maps OOXML "thin" → CSS {width: 1, style: "solid"}
|
|
135
|
+
expect(cellA1!.style.borderTop).toBeDefined();
|
|
136
|
+
expect(cellA1!.style.borderTop!.style).toBe("solid");
|
|
137
|
+
expect(cellA1!.style.borderTop!.width).toBe(1);
|
|
138
|
+
expect(cellA1!.style.borderBottom).toBeDefined();
|
|
139
|
+
expect(cellA1!.style.borderLeft).toBeDefined();
|
|
140
|
+
expect(cellA1!.style.borderRight).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("preserves thick border with color", () => {
|
|
144
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
145
|
+
const cellB1 = row1!.cells.find((c) => c.column === 2);
|
|
146
|
+
expect(cellB1).toBeDefined();
|
|
147
|
+
// Parser maps OOXML "thick" → CSS {width: 3, style: "solid"}
|
|
148
|
+
// (writer converts width=2 + style="thick" → OOXML "thick" → parser reads back as width=3 "solid")
|
|
149
|
+
expect(cellB1!.style.borderTop).toBeDefined();
|
|
150
|
+
expect(cellB1!.style.borderTop!.style).toBe("solid");
|
|
151
|
+
expect(cellB1!.style.borderTop!.width).toBeGreaterThanOrEqual(2);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("number format serialization round-trip", () => {
|
|
156
|
+
const styles = new StyleRegistry();
|
|
157
|
+
const sheet = new SheetModel("NumFmt");
|
|
158
|
+
|
|
159
|
+
sheet.set("A1", 1234.56, undefined, "#,##0.00");
|
|
160
|
+
sheet.set("A2", 0.75, undefined, "0.00%");
|
|
161
|
+
sheet.set("A3", 45000, undefined, "dd/mm/yyyy");
|
|
162
|
+
sheet.set("A4", 100); // no custom format
|
|
163
|
+
|
|
164
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
165
|
+
const xlsx = exportWorkbook(wb);
|
|
166
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
167
|
+
|
|
168
|
+
it("preserves number format on cells", () => {
|
|
169
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
170
|
+
const cellA1 = row1!.cells.find((c) => c.column === 1);
|
|
171
|
+
expect(cellA1).toBeDefined();
|
|
172
|
+
// The displayed value should be formatted
|
|
173
|
+
expect(cellA1!.value).toBe("1,234.56");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("preserves percentage format", () => {
|
|
177
|
+
const row2 = parsed.sheets[0].rows.find((r) => r.index === 2);
|
|
178
|
+
const cellA2 = row2!.cells.find((c) => c.column === 1);
|
|
179
|
+
expect(cellA2).toBeDefined();
|
|
180
|
+
// Percentage format: 0.75 → "75.00%"
|
|
181
|
+
expect(cellA2!.value).toBe("75.00%");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("preserves general format as-is", () => {
|
|
185
|
+
const row4 = parsed.sheets[0].rows.find((r) => r.index === 4);
|
|
186
|
+
const cellA4 = row4!.cells.find((c) => c.column === 1);
|
|
187
|
+
expect(cellA4).toBeDefined();
|
|
188
|
+
expect(cellA4!.value).toBe("100");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("chart serialization round-trip", () => {
|
|
193
|
+
const styles = new StyleRegistry();
|
|
194
|
+
const sheet = new SheetModel("Charts");
|
|
195
|
+
|
|
196
|
+
sheet.set("A1", "Q1");
|
|
197
|
+
sheet.set("A2", "Q2");
|
|
198
|
+
sheet.set("B1", 100);
|
|
199
|
+
sheet.set("B2", 200);
|
|
200
|
+
|
|
201
|
+
sheet.charts.push({
|
|
202
|
+
title: "Sales",
|
|
203
|
+
chartType: "col",
|
|
204
|
+
series: [{ name: "Revenue", values: [100, 200], color: "#4472C4" }],
|
|
205
|
+
categories: ["Q1", "Q2"],
|
|
206
|
+
legendPosition: "bottom",
|
|
207
|
+
anchor: { from: { row: 4, col: 0 }, to: { row: 18, col: 6 } },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
211
|
+
const xlsx = exportWorkbook(wb);
|
|
212
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
213
|
+
|
|
214
|
+
it("preserves chart with correct type and title", () => {
|
|
215
|
+
expect(parsed.sheets[0].charts).toBeDefined();
|
|
216
|
+
expect(parsed.sheets[0].charts!.length).toBe(1);
|
|
217
|
+
const chart = parsed.sheets[0].charts![0];
|
|
218
|
+
expect(chart.title).toBe("Sales");
|
|
219
|
+
expect(chart.chartType).toBe("col");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("preserves chart series data", () => {
|
|
223
|
+
const chart = parsed.sheets[0].charts![0];
|
|
224
|
+
expect(chart.series.length).toBe(1);
|
|
225
|
+
expect(chart.series[0].name).toBe("Revenue");
|
|
226
|
+
expect(chart.series[0].values).toEqual([100, 200]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("preserves chart categories", () => {
|
|
230
|
+
const chart = parsed.sheets[0].charts![0];
|
|
231
|
+
expect(chart.categories).toEqual(["Q1", "Q2"]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("table serialization round-trip", () => {
|
|
236
|
+
const styles = new StyleRegistry();
|
|
237
|
+
const sheet = new SheetModel("Tables");
|
|
238
|
+
|
|
239
|
+
sheet.set("A1", "Name");
|
|
240
|
+
sheet.set("B1", "Value");
|
|
241
|
+
sheet.set("A2", "Item1");
|
|
242
|
+
sheet.set("B2", 42);
|
|
243
|
+
|
|
244
|
+
sheet.tables.push({
|
|
245
|
+
name: "Table1",
|
|
246
|
+
displayName: "Table1",
|
|
247
|
+
ref: "A1:B2",
|
|
248
|
+
columns: [
|
|
249
|
+
{ id: 1, name: "Name" },
|
|
250
|
+
{ id: 2, name: "Value" },
|
|
251
|
+
],
|
|
252
|
+
headerRow: true,
|
|
253
|
+
totalsRow: false,
|
|
254
|
+
autoFilter: true,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
258
|
+
const xlsx = exportWorkbook(wb);
|
|
259
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
260
|
+
|
|
261
|
+
it("preserves table structure", () => {
|
|
262
|
+
expect(parsed.sheets[0].tables).toBeDefined();
|
|
263
|
+
expect(parsed.sheets[0].tables!.length).toBe(1);
|
|
264
|
+
const table = parsed.sheets[0].tables![0];
|
|
265
|
+
expect(table.name).toBe("Table1");
|
|
266
|
+
expect(table.ref).toBe("A1:B2");
|
|
267
|
+
expect(table.columns.length).toBe(2);
|
|
268
|
+
expect(table.columns[0].name).toBe("Name");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("conditional formatting serialization round-trip", () => {
|
|
273
|
+
const styles = new StyleRegistry();
|
|
274
|
+
const sheet = new SheetModel("CF");
|
|
275
|
+
|
|
276
|
+
sheet.set("A1", 10);
|
|
277
|
+
sheet.set("A2", 50);
|
|
278
|
+
sheet.set("A3", 90);
|
|
279
|
+
|
|
280
|
+
sheet.conditionalFormats.push({
|
|
281
|
+
ref: "A1:A3",
|
|
282
|
+
rules: [{
|
|
283
|
+
ruleType: "colorScale",
|
|
284
|
+
priority: 1,
|
|
285
|
+
colors: ["#FF0000", "#00FF00"],
|
|
286
|
+
}],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
290
|
+
const xlsx = exportWorkbook(wb);
|
|
291
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
292
|
+
|
|
293
|
+
it("preserves conditional formatting rules", () => {
|
|
294
|
+
expect(parsed.sheets[0].conditionalFormats).toBeDefined();
|
|
295
|
+
expect(parsed.sheets[0].conditionalFormats!.length).toBeGreaterThanOrEqual(1);
|
|
296
|
+
const cf = parsed.sheets[0].conditionalFormats![0];
|
|
297
|
+
expect(cf.ref).toBe("A1:A3");
|
|
298
|
+
expect(cf.rules.length).toBe(1);
|
|
299
|
+
expect(cf.rules[0].ruleType).toBe("colorScale");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe("hyperlink serialization round-trip", () => {
|
|
304
|
+
const styles = new StyleRegistry();
|
|
305
|
+
const sheet = new SheetModel("Links");
|
|
306
|
+
|
|
307
|
+
sheet.set("A1", "Click me");
|
|
308
|
+
sheet.hyperlinks.set("A1", "https://example.com");
|
|
309
|
+
|
|
310
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
311
|
+
const xlsx = exportWorkbook(wb);
|
|
312
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
313
|
+
|
|
314
|
+
it("preserves hyperlink on cell", () => {
|
|
315
|
+
const row1 = parsed.sheets[0].rows.find((r) => r.index === 1);
|
|
316
|
+
const cellA1 = row1!.cells.find((c) => c.column === 1);
|
|
317
|
+
expect(cellA1).toBeDefined();
|
|
318
|
+
expect(cellA1!.content.type).toBe("hyperlink");
|
|
319
|
+
if (cellA1!.content.type === "hyperlink") {
|
|
320
|
+
expect(cellA1!.content.url).toBe("https://example.com");
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("parseExcelFromZip", () => {
|
|
326
|
+
it("parses from pre-unzipped entries", () => {
|
|
327
|
+
const wb = createTestWorkbook();
|
|
328
|
+
const xlsx = exportWorkbook(wb);
|
|
329
|
+
const zip = unzipXlsx(xlsx.buffer as ArrayBuffer);
|
|
330
|
+
const parsed = parseExcelFromZip(zip);
|
|
331
|
+
expect(parsed.sheets).toHaveLength(2);
|
|
332
|
+
expect(parsed.sheets[0].name).toBe("TestSheet");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("page setup, print titles, header/footer", () => {
|
|
337
|
+
function getSheetXml(wb: WorkbookModel): string {
|
|
338
|
+
const zip = unzipXlsx(exportWorkbook(wb).buffer as ArrayBuffer);
|
|
339
|
+
return new TextDecoder().decode(zip["xl/worksheets/sheet1.xml"]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getWorkbookXml(wb: WorkbookModel): string {
|
|
343
|
+
const zip = unzipXlsx(exportWorkbook(wb).buffer as ArrayBuffer);
|
|
344
|
+
return new TextDecoder().decode(zip["xl/workbook.xml"]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
it("emits pageSetup with orientation + fit-to-page + sheetPr", () => {
|
|
348
|
+
const styles = new StyleRegistry();
|
|
349
|
+
const sheet = new SheetModel("S", styles);
|
|
350
|
+
sheet.pageSetup = { orientation: "landscape", paperSize: 9, fitToWidth: 1, fitToHeight: 0 };
|
|
351
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
352
|
+
const xml = getSheetXml(wb);
|
|
353
|
+
expect(xml).toContain(`<sheetPr><pageSetUpPr fitToPage="1"/></sheetPr>`);
|
|
354
|
+
expect(xml).toContain(`orientation="landscape"`);
|
|
355
|
+
expect(xml).toContain(`paperSize="9"`);
|
|
356
|
+
expect(xml).toContain(`fitToWidth="1"`);
|
|
357
|
+
expect(xml).toContain(`fitToHeight="0"`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("overrides pageMargins when pageSetup.margins is set", () => {
|
|
361
|
+
const styles = new StyleRegistry();
|
|
362
|
+
const sheet = new SheetModel("S", styles);
|
|
363
|
+
sheet.pageSetup = { margins: { left: 0.4, right: 0.4, top: 0.5, bottom: 0.5 } };
|
|
364
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
365
|
+
const xml = getSheetXml(wb);
|
|
366
|
+
expect(xml).toContain(`<pageMargins left="0.4" right="0.4" top="0.5" bottom="0.5" header="0.3" footer="0.3"/>`);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("emits sheet-scoped Print_Titles defined name", () => {
|
|
370
|
+
const styles = new StyleRegistry();
|
|
371
|
+
const sheet = new SheetModel("Report", styles);
|
|
372
|
+
sheet.printTitles = { repeatRows: [1, 2], repeatCols: [1, 1] };
|
|
373
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
374
|
+
const xml = getWorkbookXml(wb);
|
|
375
|
+
expect(xml).toContain(`<definedName name="_xlnm.Print_Titles" localSheetId="0">`);
|
|
376
|
+
expect(xml).toContain(`Report!$A:$A,Report!$1:$2`);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("quotes sheet names with non-alphanumerics in Print_Titles", () => {
|
|
380
|
+
const styles = new StyleRegistry();
|
|
381
|
+
const sheet = new SheetModel("Sales Q1", styles);
|
|
382
|
+
sheet.printTitles = { repeatRows: [1, 1] };
|
|
383
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
384
|
+
const xml = getWorkbookXml(wb);
|
|
385
|
+
expect(xml).toContain(`'Sales Q1'!$1:$1`);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("emits header/footer with page-field codes", () => {
|
|
389
|
+
const styles = new StyleRegistry();
|
|
390
|
+
const sheet = new SheetModel("S", styles);
|
|
391
|
+
sheet.headerFooter = {
|
|
392
|
+
oddHeader: "&LLeft&CTitle&RPage &P of &N",
|
|
393
|
+
oddFooter: "Confidential",
|
|
394
|
+
};
|
|
395
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
396
|
+
const xml = getSheetXml(wb);
|
|
397
|
+
expect(xml).toContain(`<headerFooter>`);
|
|
398
|
+
expect(xml).toContain(`<oddHeader>&LLeft&CTitle&RPage &P of &N</oddHeader>`);
|
|
399
|
+
expect(xml).toContain(`<oddFooter>Confidential</oddFooter>`);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("emits differentOddEven + differentFirst attributes", () => {
|
|
403
|
+
const styles = new StyleRegistry();
|
|
404
|
+
const sheet = new SheetModel("S", styles);
|
|
405
|
+
sheet.headerFooter = {
|
|
406
|
+
differentOddEven: true,
|
|
407
|
+
differentFirst: true,
|
|
408
|
+
oddHeader: "Odd",
|
|
409
|
+
evenHeader: "Even",
|
|
410
|
+
firstHeader: "First",
|
|
411
|
+
};
|
|
412
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
413
|
+
const xml = getSheetXml(wb);
|
|
414
|
+
expect(xml).toContain(`<headerFooter differentOddEven="1" differentFirst="1">`);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("setFreeze('B2') freezes row 1 + col A", () => {
|
|
418
|
+
const styles = new StyleRegistry();
|
|
419
|
+
const sheet = new SheetModel("S", styles);
|
|
420
|
+
sheet.setFreeze("B2");
|
|
421
|
+
expect(sheet.freeze).toEqual({ row: 1, col: 1 });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("setFreeze(null) clears the freeze", () => {
|
|
425
|
+
const styles = new StyleRegistry();
|
|
426
|
+
const sheet = new SheetModel("S", styles);
|
|
427
|
+
sheet.freeze = { row: 1, col: 0 };
|
|
428
|
+
sheet.setFreeze(null);
|
|
429
|
+
expect(sheet.freeze).toBeUndefined();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("round-trips pageSetup + headerFooter + printTitles through parser", () => {
|
|
433
|
+
const styles = new StyleRegistry();
|
|
434
|
+
const sheet = new SheetModel("Report", styles);
|
|
435
|
+
sheet.set("A1", "x");
|
|
436
|
+
sheet.pageSetup = {
|
|
437
|
+
orientation: "landscape",
|
|
438
|
+
paperSize: 9,
|
|
439
|
+
fitToWidth: 1,
|
|
440
|
+
fitToHeight: 0,
|
|
441
|
+
margins: { left: 0.4, right: 0.4, top: 0.5, bottom: 0.5, header: 0.3, footer: 0.3 },
|
|
442
|
+
};
|
|
443
|
+
sheet.printTitles = { repeatRows: [1, 2], repeatCols: [1, 1] };
|
|
444
|
+
sheet.headerFooter = {
|
|
445
|
+
oddHeader: "&LLeft&CTitle&RPage &P of &N",
|
|
446
|
+
oddFooter: "Confidential",
|
|
447
|
+
};
|
|
448
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
449
|
+
const xlsx = exportWorkbook(wb);
|
|
450
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
451
|
+
|
|
452
|
+
const s = parsed.sheets[0];
|
|
453
|
+
expect(s.pageSetup?.orientation).toBe("landscape");
|
|
454
|
+
expect(s.pageSetup?.paperSize).toBe(9);
|
|
455
|
+
expect(s.pageSetup?.fitToWidth).toBe(1);
|
|
456
|
+
expect(s.pageSetup?.fitToHeight).toBe(0);
|
|
457
|
+
expect(s.pageSetup?.margins?.left).toBeCloseTo(0.4, 3);
|
|
458
|
+
expect(s.pageSetup?.margins?.top).toBeCloseTo(0.5, 3);
|
|
459
|
+
|
|
460
|
+
expect(s.printTitles?.repeatRows).toEqual([1, 2]);
|
|
461
|
+
expect(s.printTitles?.repeatCols).toEqual([1, 1]);
|
|
462
|
+
|
|
463
|
+
expect(s.headerFooter?.oddHeader).toBe("&LLeft&CTitle&RPage &P of &N");
|
|
464
|
+
expect(s.headerFooter?.oddFooter).toBe("Confidential");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("round-trips Print_Titles with Unicode sheet name (quoted emission + parse)", () => {
|
|
468
|
+
const styles = new StyleRegistry();
|
|
469
|
+
const sheet = new SheetModel("Báo giá", styles);
|
|
470
|
+
sheet.set("A1", "x");
|
|
471
|
+
sheet.printTitles = { repeatRows: [1, 1] };
|
|
472
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
473
|
+
const xlsx = exportWorkbook(wb);
|
|
474
|
+
const parsed = parseExcelBuffer(xlsx.buffer as ArrayBuffer);
|
|
475
|
+
expect(parsed.sheets[0].printTitles?.repeatRows).toEqual([1, 1]);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("emits no pageSetup tag when pageSetup is undefined", () => {
|
|
479
|
+
const styles = new StyleRegistry();
|
|
480
|
+
const sheet = new SheetModel("S", styles);
|
|
481
|
+
sheet.set("A1", "x");
|
|
482
|
+
const wb = new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
|
|
483
|
+
const xml = getSheetXml(wb);
|
|
484
|
+
expect(xml).not.toContain(`<pageSetup`);
|
|
485
|
+
expect(xml).not.toContain(`<headerFooter`);
|
|
486
|
+
expect(xml).not.toContain(`<sheetPr>`);
|
|
487
|
+
});
|
|
488
|
+
});
|