@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.
Files changed (90) hide show
  1. package/package.json +30 -0
  2. package/src/auto_sum.test.ts +71 -0
  3. package/src/auto_sum.ts +83 -0
  4. package/src/canvas_cf.ts +135 -0
  5. package/src/canvas_chart.ts +687 -0
  6. package/src/canvas_fill.ts +156 -0
  7. package/src/canvas_images.ts +99 -0
  8. package/src/canvas_layout.test.ts +159 -0
  9. package/src/canvas_layout.ts +239 -0
  10. package/src/canvas_renderer.ts +1010 -0
  11. package/src/canvas_shape.ts +242 -0
  12. package/src/canvas_sparkline.ts +163 -0
  13. package/src/canvas_text.ts +454 -0
  14. package/src/cell_editor.ts +558 -0
  15. package/src/clipboard.test.ts +145 -0
  16. package/src/clipboard.ts +341 -0
  17. package/src/command_history.ts +102 -0
  18. package/src/csv_parser.test.ts +130 -0
  19. package/src/csv_parser.ts +172 -0
  20. package/src/data_validation.test.ts +95 -0
  21. package/src/data_validation.ts +114 -0
  22. package/src/editing_layer.test.ts +309 -0
  23. package/src/excel_cf_evaluate.test.ts +293 -0
  24. package/src/excel_cf_evaluate.ts +719 -0
  25. package/src/excel_cf_icons.ts +108 -0
  26. package/src/excel_cf_types.ts +67 -0
  27. package/src/excel_numfmt.test.ts +607 -0
  28. package/src/excel_numfmt.ts +1033 -0
  29. package/src/excel_parser.test.ts +1061 -0
  30. package/src/excel_parser.ts +393 -0
  31. package/src/excel_pattern_fills.ts +64 -0
  32. package/src/excel_table_styles.ts +160 -0
  33. package/src/excel_utils.test.ts +108 -0
  34. package/src/excel_utils.ts +162 -0
  35. package/src/fast-formula-parser.d.ts +53 -0
  36. package/src/fill_logic.ts +257 -0
  37. package/src/fill_patterns.test.ts +90 -0
  38. package/src/fill_patterns.ts +71 -0
  39. package/src/flash_fill.test.ts +75 -0
  40. package/src/flash_fill.ts +221 -0
  41. package/src/formula_deps.ts +189 -0
  42. package/src/formula_engine.test.ts +348 -0
  43. package/src/formula_engine.ts +401 -0
  44. package/src/formula_highlight.test.ts +81 -0
  45. package/src/formula_highlight.ts +139 -0
  46. package/src/formula_suggest.ts +100 -0
  47. package/src/hidden_sheets.test.ts +49 -0
  48. package/src/index.ts +149 -0
  49. package/src/keyboard_nav.test.ts +394 -0
  50. package/src/keyboard_nav.ts +891 -0
  51. package/src/move_logic.ts +63 -0
  52. package/src/numfmt_type.test.ts +158 -0
  53. package/src/numfmt_type.ts +231 -0
  54. package/src/ooxml_cell_format.ts +70 -0
  55. package/src/ooxml_chart.test.ts +85 -0
  56. package/src/ooxml_chart.ts +347 -0
  57. package/src/ooxml_chart_types.ts +207 -0
  58. package/src/ooxml_color.ts +339 -0
  59. package/src/ooxml_drawing.test.ts +87 -0
  60. package/src/ooxml_drawing.ts +287 -0
  61. package/src/ooxml_pivot.test.ts +195 -0
  62. package/src/ooxml_pivot.ts +468 -0
  63. package/src/ooxml_pivot_types.ts +165 -0
  64. package/src/ooxml_shape.ts +355 -0
  65. package/src/ooxml_sheet.ts +1271 -0
  66. package/src/ooxml_styles.ts +556 -0
  67. package/src/ooxml_table.test.ts +70 -0
  68. package/src/ooxml_table.ts +131 -0
  69. package/src/ooxml_workbook.test.ts +40 -0
  70. package/src/ooxml_workbook.ts +259 -0
  71. package/src/ooxml_zip.ts +33 -0
  72. package/src/pivot_model.ts +237 -0
  73. package/src/pivot_recompute.test.ts +210 -0
  74. package/src/pivot_recompute.ts +413 -0
  75. package/src/selection_model.ts +364 -0
  76. package/src/spreadsheet_commands.test.ts +772 -0
  77. package/src/spreadsheet_commands.ts +1805 -0
  78. package/src/structured_refs.test.ts +128 -0
  79. package/src/structured_refs.ts +160 -0
  80. package/src/style_helpers.test.ts +33 -0
  81. package/src/style_helpers.ts +30 -0
  82. package/src/template_builder.test.ts +139 -0
  83. package/src/template_builder.ts +36 -0
  84. package/src/types.ts +239 -0
  85. package/src/workbook_model.test.ts +536 -0
  86. package/src/workbook_model.ts +911 -0
  87. package/src/xlsx_round_trip.test.ts +279 -0
  88. package/src/xlsx_writer.test.ts +488 -0
  89. package/src/xlsx_writer.ts +1549 -0
  90. package/src/xml_entities.ts +23 -0
@@ -0,0 +1,772 @@
1
+ import {
2
+ setCellValueCommand,
3
+ setCellStyleCommand,
4
+ setRangeStyleCommand,
5
+ deleteCellCommand,
6
+ clearRangeCommand,
7
+ mergeCellsCommand,
8
+ unmergeCellsCommand,
9
+ setNamedRangeCommand,
10
+ setRangeNumFmtCommand,
11
+ addSheetCommand,
12
+ deleteSheetCommand,
13
+ renameSheetCommand,
14
+ setHiddenRowsCommand,
15
+ setHiddenColsCommand,
16
+ sortColumnCommand,
17
+ multiSortCommand,
18
+ setFreezePanesCommand,
19
+ setZoomCommand,
20
+ addConditionalFormatCommand,
21
+ deleteConditionalFormatCommand,
22
+ reorderSheetsCommand,
23
+ insertCellsShiftDownCommand,
24
+ insertCellsShiftRightCommand,
25
+ deleteCellsShiftUpCommand,
26
+ deleteCellsShiftLeftCommand,
27
+ setShowGridLinesCommand,
28
+ setShowHeadersCommand,
29
+ setHyperlinkCommand,
30
+ } from "./spreadsheet_commands";
31
+ import { CommandHistory } from "./command_history";
32
+ import { WorkbookModel, SheetModel, StyleRegistry } from "./workbook_model";
33
+ import type { ChangeEvent } from "./workbook_model";
34
+
35
+ function makeWorkbook(): WorkbookModel {
36
+ const styles = new StyleRegistry();
37
+ const sheet = new SheetModel("Sheet1");
38
+ return new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
39
+ }
40
+
41
+ describe("setCellValueCommand", () => {
42
+ test("sets cell value", () => {
43
+ const wb = makeWorkbook();
44
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "Hello");
45
+ cmd.execute();
46
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Hello");
47
+ });
48
+
49
+ test("sets numeric value", () => {
50
+ const wb = makeWorkbook();
51
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "42");
52
+ cmd.execute();
53
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(42);
54
+ });
55
+
56
+ test("sets boolean value", () => {
57
+ const wb = makeWorkbook();
58
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "TRUE");
59
+ cmd.execute();
60
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(true);
61
+ });
62
+
63
+ test("sets formula", () => {
64
+ const wb = makeWorkbook();
65
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "=SUM(A2:A5)");
66
+ cmd.execute();
67
+ expect(wb.sheets[0].getCell("A1")?.formula).toBe("SUM(A2:A5)");
68
+ });
69
+
70
+ test("undo restores previous value", () => {
71
+ const wb = makeWorkbook();
72
+ wb.sheets[0].set("A1", "Old");
73
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "New");
74
+ cmd.execute();
75
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("New");
76
+ cmd.undo();
77
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Old");
78
+ });
79
+
80
+ test("undo deletes cell if it was empty", () => {
81
+ const wb = makeWorkbook();
82
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "Value");
83
+ cmd.execute();
84
+ cmd.undo();
85
+ expect(wb.sheets[0].getCell("A1")).toBeUndefined();
86
+ });
87
+
88
+ test("fires change events", () => {
89
+ const wb = makeWorkbook();
90
+ const events: ChangeEvent[] = [];
91
+ wb.onChange = (e) => events.push(e);
92
+ const cmd = setCellValueCommand(wb, undefined, 0, 1, 1, "Test");
93
+ cmd.execute();
94
+ expect(events).toHaveLength(1);
95
+ expect(events[0].type).toBe("cell_value");
96
+ });
97
+
98
+ test("integrates with CommandHistory for undo/redo", () => {
99
+ const wb = makeWorkbook();
100
+ const history = new CommandHistory();
101
+ history.execute(setCellValueCommand(wb, undefined, 0, 1, 1, "First"));
102
+ history.execute(setCellValueCommand(wb, undefined, 0, 1, 1, "Second"));
103
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Second");
104
+ history.undo();
105
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("First");
106
+ history.redo();
107
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Second");
108
+ });
109
+ });
110
+
111
+ describe("setCellStyleCommand", () => {
112
+ test("applies style override", () => {
113
+ const wb = makeWorkbook();
114
+ wb.sheets[0].set("A1", "Hello");
115
+ const cmd = setCellStyleCommand(wb, 0, 1, 1, { fontBold: true });
116
+ cmd.execute();
117
+ const styleIdx = wb.sheets[0].getCell("A1")!.styleIndex;
118
+ expect(wb.styles.get(styleIdx).fontBold).toBe(true);
119
+ });
120
+
121
+ test("undo reverts style", () => {
122
+ const wb = makeWorkbook();
123
+ wb.sheets[0].set("A1", "Hello");
124
+ const cmd = setCellStyleCommand(wb, 0, 1, 1, { fontBold: true });
125
+ cmd.execute();
126
+ cmd.undo();
127
+ const styleIdx = wb.sheets[0].getCell("A1")!.styleIndex;
128
+ expect(wb.styles.get(styleIdx)).toEqual({});
129
+ });
130
+ });
131
+
132
+ describe("setRangeStyleCommand", () => {
133
+ test("applies style to all cells in range", () => {
134
+ const wb = makeWorkbook();
135
+ wb.sheets[0].set("A1", "1");
136
+ wb.sheets[0].set("B1", "2");
137
+ wb.sheets[0].set("A2", "3");
138
+ wb.sheets[0].set("B2", "4");
139
+
140
+ const cmd = setRangeStyleCommand(wb, 0, 1, 1, 2, 2, { fontBold: true });
141
+ cmd.execute();
142
+
143
+ for (const ref of ["A1", "B1", "A2", "B2"]) {
144
+ const styleIdx = wb.sheets[0].getCell(ref)!.styleIndex;
145
+ expect(wb.styles.get(styleIdx).fontBold).toBe(true);
146
+ }
147
+ });
148
+
149
+ test("undo reverts all cells", () => {
150
+ const wb = makeWorkbook();
151
+ wb.sheets[0].set("A1", "1");
152
+ wb.sheets[0].set("B1", "2");
153
+
154
+ const cmd = setRangeStyleCommand(wb, 0, 1, 1, 1, 2, { fontBold: true });
155
+ cmd.execute();
156
+ cmd.undo();
157
+
158
+ for (const ref of ["A1", "B1"]) {
159
+ const styleIdx = wb.sheets[0].getCell(ref)!.styleIndex;
160
+ expect(wb.styles.get(styleIdx).fontBold).toBeUndefined();
161
+ }
162
+ });
163
+ });
164
+
165
+ describe("deleteCellCommand", () => {
166
+ test("deletes a cell", () => {
167
+ const wb = makeWorkbook();
168
+ wb.sheets[0].set("A1", "Hello");
169
+ const cmd = deleteCellCommand(wb, undefined, 0, 1, 1);
170
+ cmd.execute();
171
+ expect(wb.sheets[0].getCell("A1")).toBeUndefined();
172
+ });
173
+
174
+ test("undo restores the cell", () => {
175
+ const wb = makeWorkbook();
176
+ wb.sheets[0].set("A1", "Hello");
177
+ const cmd = deleteCellCommand(wb, undefined, 0, 1, 1);
178
+ cmd.execute();
179
+ cmd.undo();
180
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Hello");
181
+ });
182
+ });
183
+
184
+ describe("clearRangeCommand", () => {
185
+ test("clears all cells in range", () => {
186
+ const wb = makeWorkbook();
187
+ wb.sheets[0].set("A1", "1");
188
+ wb.sheets[0].set("B1", "2");
189
+ wb.sheets[0].set("A2", "3");
190
+
191
+ const cmd = clearRangeCommand(wb, undefined, 0, 1, 1, 2, 2);
192
+ cmd.execute();
193
+
194
+ expect(wb.sheets[0].getCell("A1")).toBeUndefined();
195
+ expect(wb.sheets[0].getCell("B1")).toBeUndefined();
196
+ expect(wb.sheets[0].getCell("A2")).toBeUndefined();
197
+ });
198
+
199
+ test("undo restores all cells", () => {
200
+ const wb = makeWorkbook();
201
+ wb.sheets[0].set("A1", "1");
202
+ wb.sheets[0].set("B1", "2");
203
+
204
+ const cmd = clearRangeCommand(wb, undefined, 0, 1, 1, 1, 2);
205
+ cmd.execute();
206
+ cmd.undo();
207
+
208
+ expect(wb.sheets[0].getCell("A1")?.displayValue).toBe("1");
209
+ expect(wb.sheets[0].getCell("B1")?.displayValue).toBe("2");
210
+ });
211
+ });
212
+
213
+ describe("mergeCellsCommand", () => {
214
+ test("adds merge range", () => {
215
+ const wb = makeWorkbook();
216
+ const cmd = mergeCellsCommand(wb, 0, 1, 1, 1, 3);
217
+ cmd.execute();
218
+ expect(wb.sheets[0].mergedCells).toContain("A1:C1");
219
+ });
220
+
221
+ test("undo removes merge range", () => {
222
+ const wb = makeWorkbook();
223
+ const cmd = mergeCellsCommand(wb, 0, 1, 1, 1, 3);
224
+ cmd.execute();
225
+ cmd.undo();
226
+ expect(wb.sheets[0].mergedCells).not.toContain("A1:C1");
227
+ });
228
+ });
229
+
230
+ describe("unmergeCellsCommand", () => {
231
+ test("removes merge range", () => {
232
+ const wb = makeWorkbook();
233
+ wb.sheets[0].mergedCells.push("A1:C1");
234
+ const cmd = unmergeCellsCommand(wb, 0, "A1:C1");
235
+ cmd.execute();
236
+ expect(wb.sheets[0].mergedCells).not.toContain("A1:C1");
237
+ });
238
+
239
+ test("undo re-adds merge range", () => {
240
+ const wb = makeWorkbook();
241
+ wb.sheets[0].mergedCells.push("A1:C1");
242
+ const cmd = unmergeCellsCommand(wb, 0, "A1:C1");
243
+ cmd.execute();
244
+ cmd.undo();
245
+ expect(wb.sheets[0].mergedCells).toContain("A1:C1");
246
+ });
247
+ });
248
+
249
+ describe("setNamedRangeCommand", () => {
250
+ test("creates a named range", () => {
251
+ const wb = makeWorkbook();
252
+ const cmd = setNamedRangeCommand(wb, "TotalRevenue", "Sheet1!$C$11");
253
+ cmd.execute();
254
+ expect(wb.namedRanges.get("TotalRevenue")).toBe("Sheet1!$C$11");
255
+ });
256
+
257
+ test("undo removes a newly created named range", () => {
258
+ const wb = makeWorkbook();
259
+ const cmd = setNamedRangeCommand(wb, "MyRange", "Sheet1!$A$1:$C$10");
260
+ cmd.execute();
261
+ cmd.undo();
262
+ expect(wb.namedRanges.has("MyRange")).toBe(false);
263
+ });
264
+
265
+ test("undo restores previous value when overwriting", () => {
266
+ const wb = makeWorkbook();
267
+ wb.namedRanges.set("MyRange", "Sheet1!$A$1");
268
+ const cmd = setNamedRangeCommand(wb, "MyRange", "Sheet1!$B$2:$D$5");
269
+ cmd.execute();
270
+ expect(wb.namedRanges.get("MyRange")).toBe("Sheet1!$B$2:$D$5");
271
+ cmd.undo();
272
+ expect(wb.namedRanges.get("MyRange")).toBe("Sheet1!$A$1");
273
+ });
274
+
275
+ test("redo re-applies the named range", () => {
276
+ const wb = makeWorkbook();
277
+ const history = new CommandHistory();
278
+ history.execute(setNamedRangeCommand(wb, "Test", "Sheet1!$A$1"));
279
+ history.undo();
280
+ expect(wb.namedRanges.has("Test")).toBe(false);
281
+ history.redo();
282
+ expect(wb.namedRanges.get("Test")).toBe("Sheet1!$A$1");
283
+ });
284
+ });
285
+
286
+ describe("setRangeNumFmtCommand", () => {
287
+ test("changes number format for a cell", () => {
288
+ const wb = makeWorkbook();
289
+ wb.sheets[0].set("A1", 42);
290
+ const cmd = setRangeNumFmtCommand(wb, 0, 1, 1, 1, 1, "0.00");
291
+ cmd.execute();
292
+ expect(wb.sheets[0].getCell("A1")?.numFmtCode).toBe("0.00");
293
+ });
294
+
295
+ test("applies to a range", () => {
296
+ const wb = makeWorkbook();
297
+ wb.sheets[0].set("A1", 1);
298
+ wb.sheets[0].set("B1", 2);
299
+ wb.sheets[0].set("A2", 3);
300
+ const cmd = setRangeNumFmtCommand(wb, 0, 1, 1, 2, 2, "$#,##0.00");
301
+ cmd.execute();
302
+ expect(wb.sheets[0].getCell("A1")?.numFmtCode).toBe("$#,##0.00");
303
+ expect(wb.sheets[0].getCell("B1")?.numFmtCode).toBe("$#,##0.00");
304
+ expect(wb.sheets[0].getCell("A2")?.numFmtCode).toBe("$#,##0.00");
305
+ });
306
+
307
+ test("undo restores previous format", () => {
308
+ const wb = makeWorkbook();
309
+ wb.sheets[0].set("A1", 42);
310
+ const cmd = setRangeNumFmtCommand(wb, 0, 1, 1, 1, 1, "0.00%");
311
+ cmd.execute();
312
+ cmd.undo();
313
+ expect(wb.sheets[0].getCell("A1")?.numFmtCode).toBe("General");
314
+ });
315
+
316
+ test("creates empty cell with format if cell was empty", () => {
317
+ const wb = makeWorkbook();
318
+ const cmd = setRangeNumFmtCommand(wb, 0, 1, 1, 1, 1, "0.00");
319
+ cmd.execute();
320
+ expect(wb.sheets[0].getCell("A1")?.numFmtCode).toBe("0.00");
321
+ });
322
+
323
+ test("undo removes cell if it was empty before", () => {
324
+ const wb = makeWorkbook();
325
+ const cmd = setRangeNumFmtCommand(wb, 0, 1, 1, 1, 1, "0.00");
326
+ cmd.execute();
327
+ cmd.undo();
328
+ expect(wb.sheets[0].getCell("A1")).toBeUndefined();
329
+ });
330
+ });
331
+
332
+ describe("addSheetCommand", () => {
333
+ test("adds a new sheet", () => {
334
+ const wb = makeWorkbook();
335
+ const cmd = addSheetCommand(wb, "Sheet2");
336
+ cmd.execute();
337
+ expect(wb.sheets).toHaveLength(2);
338
+ expect(wb.sheets[1].name).toBe("Sheet2");
339
+ });
340
+
341
+ test("undo removes the added sheet", () => {
342
+ const wb = makeWorkbook();
343
+ const cmd = addSheetCommand(wb, "Sheet2");
344
+ cmd.execute();
345
+ cmd.undo();
346
+ expect(wb.sheets).toHaveLength(1);
347
+ });
348
+ });
349
+
350
+ describe("deleteSheetCommand", () => {
351
+ test("deletes a sheet", () => {
352
+ const wb = makeWorkbook();
353
+ wb.sheets.push(new (wb.sheets[0].constructor as new (name: string) => typeof wb.sheets[0])("Sheet2"));
354
+ const cmd = deleteSheetCommand(wb, 1);
355
+ cmd.execute();
356
+ expect(wb.sheets).toHaveLength(1);
357
+ });
358
+
359
+ test("undo restores the deleted sheet", () => {
360
+ const wb = makeWorkbook();
361
+ const originalSheet = wb.sheets[0];
362
+ originalSheet.set("A1", "Keep");
363
+ const cmd = deleteSheetCommand(wb, 0);
364
+ cmd.execute();
365
+ expect(wb.sheets).toHaveLength(0);
366
+ cmd.undo();
367
+ expect(wb.sheets).toHaveLength(1);
368
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Keep");
369
+ });
370
+ });
371
+
372
+ describe("renameSheetCommand", () => {
373
+ test("renames a sheet", () => {
374
+ const wb = makeWorkbook();
375
+ const cmd = renameSheetCommand(wb, 0, "Renamed");
376
+ cmd.execute();
377
+ expect(wb.sheets[0].name).toBe("Renamed");
378
+ });
379
+
380
+ test("undo restores original name", () => {
381
+ const wb = makeWorkbook();
382
+ const cmd = renameSheetCommand(wb, 0, "Renamed");
383
+ cmd.execute();
384
+ cmd.undo();
385
+ expect(wb.sheets[0].name).toBe("Sheet1");
386
+ });
387
+ });
388
+
389
+ // =============================================================================
390
+ // New commands
391
+ // =============================================================================
392
+
393
+ describe("setHiddenRowsCommand", () => {
394
+ test("sets hidden rows", () => {
395
+ const wb = makeWorkbook();
396
+ const cmd = setHiddenRowsCommand(wb, 0, new Set([2, 3]));
397
+ cmd.execute();
398
+ expect(wb.sheets[0].hiddenRows).toEqual(new Set([2, 3]));
399
+ });
400
+
401
+ test("undo restores previous hidden rows", () => {
402
+ const wb = makeWorkbook();
403
+ wb.sheets[0].hiddenRows = new Set([5]);
404
+ const cmd = setHiddenRowsCommand(wb, 0, new Set([2, 3]));
405
+ cmd.execute();
406
+ cmd.undo();
407
+ expect(wb.sheets[0].hiddenRows).toEqual(new Set([5]));
408
+ });
409
+
410
+ test("redo re-applies", () => {
411
+ const wb = makeWorkbook();
412
+ const history = new CommandHistory();
413
+ history.execute(setHiddenRowsCommand(wb, 0, new Set([1, 2])));
414
+ history.undo();
415
+ expect(wb.sheets[0].hiddenRows.size).toBe(0);
416
+ history.redo();
417
+ expect(wb.sheets[0].hiddenRows).toEqual(new Set([1, 2]));
418
+ });
419
+ });
420
+
421
+ describe("setHiddenColsCommand", () => {
422
+ test("sets hidden cols", () => {
423
+ const wb = makeWorkbook();
424
+ const cmd = setHiddenColsCommand(wb, 0, new Set([1, 4]));
425
+ cmd.execute();
426
+ expect(wb.sheets[0].hiddenCols).toEqual(new Set([1, 4]));
427
+ });
428
+
429
+ test("undo restores previous hidden cols", () => {
430
+ const wb = makeWorkbook();
431
+ wb.sheets[0].hiddenCols = new Set([3]);
432
+ const cmd = setHiddenColsCommand(wb, 0, new Set([1]));
433
+ cmd.execute();
434
+ cmd.undo();
435
+ expect(wb.sheets[0].hiddenCols).toEqual(new Set([3]));
436
+ });
437
+ });
438
+
439
+ describe("sortColumnCommand", () => {
440
+ test("sorts rows ascending", () => {
441
+ const wb = makeWorkbook();
442
+ wb.sheets[0].set("A1", 3);
443
+ wb.sheets[0].set("A2", 1);
444
+ wb.sheets[0].set("A3", 2);
445
+ const cmd = sortColumnCommand(wb, 0, 1, true);
446
+ cmd.execute();
447
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(1);
448
+ expect(wb.sheets[0].getCell("A2")?.value).toBe(2);
449
+ expect(wb.sheets[0].getCell("A3")?.value).toBe(3);
450
+ });
451
+
452
+ test("undo restores original order", () => {
453
+ const wb = makeWorkbook();
454
+ wb.sheets[0].set("A1", 3);
455
+ wb.sheets[0].set("A2", 1);
456
+ const cmd = sortColumnCommand(wb, 0, 1, true);
457
+ cmd.execute();
458
+ cmd.undo();
459
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(3);
460
+ expect(wb.sheets[0].getCell("A2")?.value).toBe(1);
461
+ });
462
+ });
463
+
464
+ describe("multiSortCommand", () => {
465
+ test("sorts by 2 keys (primary asc, secondary desc)", () => {
466
+ const wb = makeWorkbook();
467
+ // Col A = category, Col B = value
468
+ wb.sheets[0].set("A1", "B");
469
+ wb.sheets[0].set("B1", 3);
470
+ wb.sheets[0].set("A2", "A");
471
+ wb.sheets[0].set("B2", 1);
472
+ wb.sheets[0].set("A3", "A");
473
+ wb.sheets[0].set("B3", 2);
474
+ wb.sheets[0].set("A4", "B");
475
+ wb.sheets[0].set("B4", 1);
476
+
477
+ const cmd = multiSortCommand(wb, 0, [
478
+ { col: 1, ascending: true }, // Sort by col A asc
479
+ { col: 2, ascending: false }, // Then by col B desc
480
+ ], false);
481
+ cmd.execute();
482
+
483
+ // A asc: A,A,B,B — within A: B desc: 2,1 — within B: B desc: 3,1
484
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("A");
485
+ expect(wb.sheets[0].getCell("B1")?.value).toBe(2);
486
+ expect(wb.sheets[0].getCell("A2")?.value).toBe("A");
487
+ expect(wb.sheets[0].getCell("B2")?.value).toBe(1);
488
+ expect(wb.sheets[0].getCell("A3")?.value).toBe("B");
489
+ expect(wb.sheets[0].getCell("B3")?.value).toBe(3);
490
+ expect(wb.sheets[0].getCell("A4")?.value).toBe("B");
491
+ expect(wb.sheets[0].getCell("B4")?.value).toBe(1);
492
+ });
493
+
494
+ test("undo restores original order", () => {
495
+ const wb = makeWorkbook();
496
+ wb.sheets[0].set("A1", 3);
497
+ wb.sheets[0].set("A2", 1);
498
+ wb.sheets[0].set("A3", 2);
499
+
500
+ const cmd = multiSortCommand(wb, 0, [{ col: 1, ascending: true }], false);
501
+ cmd.execute();
502
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(1);
503
+ cmd.undo();
504
+ expect(wb.sheets[0].getCell("A1")?.value).toBe(3);
505
+ expect(wb.sheets[0].getCell("A2")?.value).toBe(1);
506
+ expect(wb.sheets[0].getCell("A3")?.value).toBe(2);
507
+ });
508
+
509
+ test("hasHeader skips first row", () => {
510
+ const wb = makeWorkbook();
511
+ wb.sheets[0].set("A1", "Header");
512
+ wb.sheets[0].set("A2", 3);
513
+ wb.sheets[0].set("A3", 1);
514
+ wb.sheets[0].set("A4", 2);
515
+
516
+ const cmd = multiSortCommand(wb, 0, [{ col: 1, ascending: true }], true);
517
+ cmd.execute();
518
+
519
+ // Header stays at row 1, data rows 2-4 get sorted
520
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Header");
521
+ expect(wb.sheets[0].getCell("A2")?.value).toBe(1);
522
+ expect(wb.sheets[0].getCell("A3")?.value).toBe(2);
523
+ expect(wb.sheets[0].getCell("A4")?.value).toBe(3);
524
+ });
525
+ });
526
+
527
+ describe("setFreezePanesCommand", () => {
528
+ test("sets freeze panes", () => {
529
+ const wb = makeWorkbook();
530
+ const cmd = setFreezePanesCommand(wb, 0, { row: 2, col: 1 });
531
+ cmd.execute();
532
+ expect(wb.sheets[0].freeze).toEqual({ row: 2, col: 1 });
533
+ });
534
+
535
+ test("clears freeze panes", () => {
536
+ const wb = makeWorkbook();
537
+ wb.sheets[0].freeze = { row: 3, col: 2 };
538
+ const cmd = setFreezePanesCommand(wb, 0, undefined);
539
+ cmd.execute();
540
+ expect(wb.sheets[0].freeze).toBeUndefined();
541
+ });
542
+
543
+ test("undo restores previous freeze", () => {
544
+ const wb = makeWorkbook();
545
+ wb.sheets[0].freeze = { row: 1, col: 1 };
546
+ const cmd = setFreezePanesCommand(wb, 0, { row: 3, col: 3 });
547
+ cmd.execute();
548
+ cmd.undo();
549
+ expect(wb.sheets[0].freeze).toEqual({ row: 1, col: 1 });
550
+ });
551
+ });
552
+
553
+ describe("setZoomCommand", () => {
554
+ test("sets zoom level", () => {
555
+ const wb = makeWorkbook();
556
+ const cmd = setZoomCommand(wb, 0, 150);
557
+ cmd.execute();
558
+ expect(wb.sheets[0].view.zoomScale).toBe(150);
559
+ });
560
+
561
+ test("undo restores previous zoom", () => {
562
+ const wb = makeWorkbook();
563
+ wb.sheets[0].view.zoomScale = 80;
564
+ const cmd = setZoomCommand(wb, 0, 120);
565
+ cmd.execute();
566
+ cmd.undo();
567
+ expect(wb.sheets[0].view.zoomScale).toBe(80);
568
+ });
569
+ });
570
+
571
+ describe("addConditionalFormatCommand", () => {
572
+ test("adds a CF rule", () => {
573
+ const wb = makeWorkbook();
574
+ const cf = { ref: "A1:B5", rules: [{ ruleType: "colorScale" as const, priority: 1, colors: ["#F00", "#0F0", "#00F"] }] };
575
+ const cmd = addConditionalFormatCommand(wb, 0, cf);
576
+ cmd.execute();
577
+ expect(wb.sheets[0].conditionalFormats).toHaveLength(1);
578
+ expect(wb.sheets[0].conditionalFormats[0].ref).toBe("A1:B5");
579
+ });
580
+
581
+ test("undo removes the added rule", () => {
582
+ const wb = makeWorkbook();
583
+ const cf = { ref: "A1:B5", rules: [{ ruleType: "colorScale" as const, priority: 1, colors: ["#F00", "#0F0", "#00F"] }] };
584
+ const cmd = addConditionalFormatCommand(wb, 0, cf);
585
+ cmd.execute();
586
+ cmd.undo();
587
+ expect(wb.sheets[0].conditionalFormats).toHaveLength(0);
588
+ });
589
+ });
590
+
591
+ describe("deleteConditionalFormatCommand", () => {
592
+ test("deletes a CF rule", () => {
593
+ const wb = makeWorkbook();
594
+ wb.sheets[0].conditionalFormats.push({ ref: "A1:A10", rules: [{ ruleType: "colorScale", priority: 1, colors: ["#F00", "#0F0", "#00F"] }] });
595
+ const cmd = deleteConditionalFormatCommand(wb, 0, 0);
596
+ cmd.execute();
597
+ expect(wb.sheets[0].conditionalFormats).toHaveLength(0);
598
+ });
599
+
600
+ test("undo restores deleted rule", () => {
601
+ const wb = makeWorkbook();
602
+ wb.sheets[0].conditionalFormats.push({ ref: "A1:A10", rules: [{ ruleType: "colorScale", priority: 1, colors: ["#F00", "#0F0", "#00F"] }] });
603
+ const cmd = deleteConditionalFormatCommand(wb, 0, 0);
604
+ cmd.execute();
605
+ cmd.undo();
606
+ expect(wb.sheets[0].conditionalFormats).toHaveLength(1);
607
+ expect(wb.sheets[0].conditionalFormats[0].ref).toBe("A1:A10");
608
+ });
609
+ });
610
+
611
+ describe("reorderSheetsCommand", () => {
612
+ test("moves a sheet", () => {
613
+ const wb = makeWorkbook();
614
+ wb.sheets.push(new SheetModel("Sheet2"));
615
+ wb.sheets.push(new SheetModel("Sheet3"));
616
+ const cmd = reorderSheetsCommand(wb, 0, 2);
617
+ cmd.execute();
618
+ expect(wb.sheets.map((s) => s.name)).toEqual(["Sheet2", "Sheet3", "Sheet1"]);
619
+ });
620
+
621
+ test("undo reverses the move", () => {
622
+ const wb = makeWorkbook();
623
+ wb.sheets.push(new SheetModel("Sheet2"));
624
+ const cmd = reorderSheetsCommand(wb, 0, 1);
625
+ cmd.execute();
626
+ cmd.undo();
627
+ expect(wb.sheets.map((s) => s.name)).toEqual(["Sheet1", "Sheet2"]);
628
+ });
629
+ });
630
+
631
+ describe("insertCellsShiftDownCommand", () => {
632
+ test("shifts cells down", () => {
633
+ const wb = makeWorkbook();
634
+ wb.sheets[0].set("A1", "Top");
635
+ wb.sheets[0].set("A2", "Middle");
636
+ wb.sheets[0].set("A3", "Bottom");
637
+ const cmd = insertCellsShiftDownCommand(wb, 0, 2, 1, 2, 1);
638
+ cmd.execute();
639
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Top");
640
+ expect(wb.sheets[0].getCell("A2")).toBeUndefined();
641
+ expect(wb.sheets[0].getCell("A3")?.value).toBe("Middle");
642
+ expect(wb.sheets[0].getCell("A4")?.value).toBe("Bottom");
643
+ });
644
+
645
+ test("undo restores cells", () => {
646
+ const wb = makeWorkbook();
647
+ wb.sheets[0].set("A1", "Top");
648
+ wb.sheets[0].set("A2", "Middle");
649
+ const cmd = insertCellsShiftDownCommand(wb, 0, 1, 1, 1, 1);
650
+ cmd.execute();
651
+ cmd.undo();
652
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Top");
653
+ expect(wb.sheets[0].getCell("A2")?.value).toBe("Middle");
654
+ });
655
+ });
656
+
657
+ describe("insertCellsShiftRightCommand", () => {
658
+ test("shifts cells right", () => {
659
+ const wb = makeWorkbook();
660
+ wb.sheets[0].set("A1", "Left");
661
+ wb.sheets[0].set("B1", "Right");
662
+ const cmd = insertCellsShiftRightCommand(wb, 0, 1, 1, 1, 1);
663
+ cmd.execute();
664
+ expect(wb.sheets[0].getCell("A1")).toBeUndefined();
665
+ expect(wb.sheets[0].getCell("B1")?.value).toBe("Left");
666
+ expect(wb.sheets[0].getCell("C1")?.value).toBe("Right");
667
+ });
668
+ });
669
+
670
+ describe("deleteCellsShiftUpCommand", () => {
671
+ test("deletes cells and shifts up", () => {
672
+ const wb = makeWorkbook();
673
+ wb.sheets[0].set("A1", "One");
674
+ wb.sheets[0].set("A2", "Two");
675
+ wb.sheets[0].set("A3", "Three");
676
+ const cmd = deleteCellsShiftUpCommand(wb, 0, 1, 1, 1, 1);
677
+ cmd.execute();
678
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Two");
679
+ expect(wb.sheets[0].getCell("A2")?.value).toBe("Three");
680
+ expect(wb.sheets[0].getCell("A3")).toBeUndefined();
681
+ });
682
+
683
+ test("undo restores deleted cells and shifts back", () => {
684
+ const wb = makeWorkbook();
685
+ wb.sheets[0].set("A1", "One");
686
+ wb.sheets[0].set("A2", "Two");
687
+ const cmd = deleteCellsShiftUpCommand(wb, 0, 1, 1, 1, 1);
688
+ cmd.execute();
689
+ cmd.undo();
690
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("One");
691
+ expect(wb.sheets[0].getCell("A2")?.value).toBe("Two");
692
+ });
693
+ });
694
+
695
+ describe("deleteCellsShiftLeftCommand", () => {
696
+ test("deletes cells and shifts left", () => {
697
+ const wb = makeWorkbook();
698
+ wb.sheets[0].set("A1", "Left");
699
+ wb.sheets[0].set("B1", "Mid");
700
+ wb.sheets[0].set("C1", "Right");
701
+ const cmd = deleteCellsShiftLeftCommand(wb, 0, 1, 1, 1, 1);
702
+ cmd.execute();
703
+ expect(wb.sheets[0].getCell("A1")?.value).toBe("Mid");
704
+ expect(wb.sheets[0].getCell("B1")?.value).toBe("Right");
705
+ expect(wb.sheets[0].getCell("C1")).toBeUndefined();
706
+ });
707
+ });
708
+
709
+ describe("setShowGridLinesCommand", () => {
710
+ test("toggles grid lines off", () => {
711
+ const wb = makeWorkbook();
712
+ expect(wb.sheets[0].view.showGridLines).toBe(true);
713
+ const cmd = setShowGridLinesCommand(wb, 0, false);
714
+ cmd.execute();
715
+ expect(wb.sheets[0].view.showGridLines).toBe(false);
716
+ });
717
+
718
+ test("undo restores grid lines", () => {
719
+ const wb = makeWorkbook();
720
+ const cmd = setShowGridLinesCommand(wb, 0, false);
721
+ cmd.execute();
722
+ cmd.undo();
723
+ expect(wb.sheets[0].view.showGridLines).toBe(true);
724
+ });
725
+ });
726
+
727
+ describe("setShowHeadersCommand", () => {
728
+ test("toggles headers off", () => {
729
+ const wb = makeWorkbook();
730
+ expect(wb.sheets[0].view.showHeaders).toBe(true);
731
+ const cmd = setShowHeadersCommand(wb, 0, false);
732
+ cmd.execute();
733
+ expect(wb.sheets[0].view.showHeaders).toBe(false);
734
+ });
735
+
736
+ test("undo restores headers", () => {
737
+ const wb = makeWorkbook();
738
+ const cmd = setShowHeadersCommand(wb, 0, false);
739
+ cmd.execute();
740
+ cmd.undo();
741
+ expect(wb.sheets[0].view.showHeaders).toBe(true);
742
+ });
743
+ });
744
+
745
+ describe("setHyperlinkCommand", () => {
746
+ test("sets hyperlink on cell", () => {
747
+ const wb = makeWorkbook();
748
+ wb.sheets[0].set("A1", "Click me");
749
+ const cmd = setHyperlinkCommand(wb, 0, "A1", "https://example.com");
750
+ cmd.execute();
751
+ expect(wb.sheets[0].hyperlinks.get("A1")).toBe("https://example.com");
752
+ expect(wb.sheets[0].getCell("A1")?.content.type).toBe("hyperlink");
753
+ });
754
+
755
+ test("removes hyperlink", () => {
756
+ const wb = makeWorkbook();
757
+ wb.sheets[0].set("A1", "Click me");
758
+ wb.sheets[0].hyperlinks.set("A1", "https://example.com");
759
+ const cmd = setHyperlinkCommand(wb, 0, "A1", undefined);
760
+ cmd.execute();
761
+ expect(wb.sheets[0].hyperlinks.has("A1")).toBe(false);
762
+ });
763
+
764
+ test("undo restores previous hyperlink state", () => {
765
+ const wb = makeWorkbook();
766
+ wb.sheets[0].set("A1", "Click me");
767
+ const cmd = setHyperlinkCommand(wb, 0, "A1", "https://example.com");
768
+ cmd.execute();
769
+ cmd.undo();
770
+ expect(wb.sheets[0].hyperlinks.has("A1")).toBe(false);
771
+ });
772
+ });