@simplysm/excel 13.0.100 → 14.0.4
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/README.md +66 -78
- package/dist/excel-cell.d.ts +28 -28
- package/dist/excel-cell.d.ts.map +1 -1
- package/dist/excel-cell.js +273 -264
- package/dist/excel-cell.js.map +1 -6
- package/dist/excel-col.d.ts +4 -4
- package/dist/excel-col.d.ts.map +1 -1
- package/dist/excel-col.js +33 -35
- package/dist/excel-col.js.map +1 -6
- package/dist/excel-row.d.ts +3 -3
- package/dist/excel-row.d.ts.map +1 -1
- package/dist/excel-row.js +28 -30
- package/dist/excel-row.js.map +1 -6
- package/dist/excel-workbook.d.ts +23 -23
- package/dist/excel-workbook.d.ts.map +1 -1
- package/dist/excel-workbook.js +151 -125
- package/dist/excel-workbook.js.map +1 -6
- package/dist/excel-worksheet.d.ts +32 -32
- package/dist/excel-worksheet.d.ts.map +1 -1
- package/dist/excel-worksheet.js +281 -253
- package/dist/excel-worksheet.js.map +1 -6
- package/dist/excel-wrapper.d.ts +7 -7
- package/dist/excel-wrapper.d.ts.map +1 -1
- package/dist/excel-wrapper.js +190 -226
- package/dist/excel-wrapper.js.map +1 -6
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -6
- package/dist/types.d.ts +13 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -1
- package/dist/types.js.map +1 -6
- package/dist/utils/excel-utils.d.ts +23 -23
- package/dist/utils/excel-utils.d.ts.map +1 -1
- package/dist/utils/excel-utils.js +183 -151
- package/dist/utils/excel-utils.js.map +1 -6
- package/dist/utils/zip-cache.d.ts +6 -6
- package/dist/utils/zip-cache.js +78 -60
- package/dist/utils/zip-cache.js.map +1 -6
- package/dist/xml/excel-xml-content-type.d.ts +2 -2
- package/dist/xml/excel-xml-content-type.js +55 -53
- package/dist/xml/excel-xml-content-type.js.map +1 -6
- package/dist/xml/excel-xml-drawing.d.ts +2 -2
- package/dist/xml/excel-xml-drawing.js +74 -73
- package/dist/xml/excel-xml-drawing.js.map +1 -6
- package/dist/xml/excel-xml-relationship.d.ts +2 -2
- package/dist/xml/excel-xml-relationship.js +67 -67
- package/dist/xml/excel-xml-relationship.js.map +1 -6
- package/dist/xml/excel-xml-shared-string.d.ts +2 -2
- package/dist/xml/excel-xml-shared-string.js +57 -55
- package/dist/xml/excel-xml-shared-string.js.map +1 -6
- package/dist/xml/excel-xml-style.d.ts +2 -2
- package/dist/xml/excel-xml-style.js +311 -295
- package/dist/xml/excel-xml-style.js.map +1 -6
- package/dist/xml/excel-xml-unknown.d.ts +2 -2
- package/dist/xml/excel-xml-unknown.js +11 -10
- package/dist/xml/excel-xml-unknown.js.map +1 -6
- package/dist/xml/excel-xml-workbook.d.ts +3 -2
- package/dist/xml/excel-xml-workbook.d.ts.map +1 -1
- package/dist/xml/excel-xml-workbook.js +95 -90
- package/dist/xml/excel-xml-workbook.js.map +1 -6
- package/dist/xml/excel-xml-worksheet.d.ts +6 -6
- package/dist/xml/excel-xml-worksheet.js +450 -393
- package/dist/xml/excel-xml-worksheet.js.map +1 -6
- package/docs/core.md +147 -0
- package/docs/types.md +201 -212
- package/docs/wrapper.md +32 -62
- package/package.json +8 -6
- package/src/excel-cell.ts +36 -36
- package/src/excel-col.ts +4 -4
- package/src/excel-row.ts +3 -3
- package/src/excel-workbook.ts +38 -38
- package/src/excel-worksheet.ts +69 -51
- package/src/excel-wrapper.ts +55 -50
- package/src/index.ts +3 -3
- package/src/types.ts +17 -17
- package/src/utils/excel-utils.ts +47 -41
- package/src/utils/zip-cache.ts +6 -6
- package/src/xml/excel-xml-content-type.ts +3 -3
- package/src/xml/excel-xml-drawing.ts +2 -2
- package/src/xml/excel-xml-relationship.ts +3 -3
- package/src/xml/excel-xml-shared-string.ts +2 -2
- package/src/xml/excel-xml-style.ts +13 -13
- package/src/xml/excel-xml-unknown.ts +2 -2
- package/src/xml/excel-xml-workbook.ts +14 -6
- package/src/xml/excel-xml-worksheet.ts +43 -43
- package/docs/core-classes.md +0 -541
- package/docs/utilities.md +0 -128
- package/tests/excel-cell.spec.ts +0 -393
- package/tests/excel-col.spec.ts +0 -81
- package/tests/excel-row.spec.ts +0 -61
- package/tests/excel-workbook.spec.ts +0 -205
- package/tests/excel-worksheet.spec.ts +0 -469
- package/tests/excel-wrapper.spec.ts +0 -273
- package/tests/fixtures/logo.png +0 -0
- package/tests/fixtures//354/264/210/352/270/260/355/231/224.xlsx +0 -0
- package/tests/image-insert.spec.ts +0 -190
- package/tests/utils/excel-utils.spec.ts +0 -198
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { ExcelWrapper } from "../src/excel-wrapper";
|
|
4
|
-
import { DateOnly, DateTime, Time } from "@simplysm/core-common";
|
|
5
|
-
|
|
6
|
-
describe("ExcelWrapper", () => {
|
|
7
|
-
const testSchema = z.object({
|
|
8
|
-
name: z.string().describe("Name"),
|
|
9
|
-
age: z.number().describe("Age"),
|
|
10
|
-
email: z.string().optional().describe("Email"),
|
|
11
|
-
active: z.boolean().default(false).describe("Active"),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe("write", () => {
|
|
15
|
-
it("can convert records to Excel", async () => {
|
|
16
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
17
|
-
|
|
18
|
-
const records = [
|
|
19
|
-
{ name: "John Doe", age: 30, email: "john@test.com", active: true },
|
|
20
|
-
{ name: "Jane Smith", age: 25 },
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
const wb = await wrapper.write("Users", records);
|
|
24
|
-
const ws = await wb.getWorksheet("Users");
|
|
25
|
-
|
|
26
|
-
// Check headers
|
|
27
|
-
expect(await ws.cell(0, 0).getValue()).toBe("Name");
|
|
28
|
-
expect(await ws.cell(0, 1).getValue()).toBe("Age");
|
|
29
|
-
expect(await ws.cell(0, 2).getValue()).toBe("Email");
|
|
30
|
-
expect(await ws.cell(0, 3).getValue()).toBe("Active");
|
|
31
|
-
|
|
32
|
-
// Check data
|
|
33
|
-
expect(await ws.cell(1, 0).getValue()).toBe("John Doe");
|
|
34
|
-
expect(await ws.cell(1, 1).getValue()).toBe(30);
|
|
35
|
-
expect(await ws.cell(2, 0).getValue()).toBe("Jane Smith");
|
|
36
|
-
expect(await ws.cell(2, 1).getValue()).toBe(25);
|
|
37
|
-
|
|
38
|
-
await wb.close();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("applies yellow background to required fields", async () => {
|
|
42
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
43
|
-
const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
|
|
44
|
-
const ws = await wb.getWorksheet("Test");
|
|
45
|
-
|
|
46
|
-
// Required fields (name, age) have style applied
|
|
47
|
-
const nameStyleId = await ws.cell(0, 0).getStyleId();
|
|
48
|
-
const ageStyleId = await ws.cell(0, 1).getStyleId();
|
|
49
|
-
|
|
50
|
-
expect(nameStyleId).toBeDefined();
|
|
51
|
-
expect(ageStyleId).toBeDefined();
|
|
52
|
-
|
|
53
|
-
await wb.close();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("read", () => {
|
|
58
|
-
it("can read records from Excel", async () => {
|
|
59
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
60
|
-
|
|
61
|
-
// Create Excel first
|
|
62
|
-
const records = [
|
|
63
|
-
{ name: "John Doe", age: 30, email: "john@test.com", active: true },
|
|
64
|
-
{ name: "Jane Smith", age: 25, active: false },
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
const wb = await wrapper.write("Users", records);
|
|
68
|
-
const buffer = await wb.toBytes();
|
|
69
|
-
await wb.close();
|
|
70
|
-
|
|
71
|
-
// Read from Excel
|
|
72
|
-
const readRecords = await wrapper.read(buffer, "Users");
|
|
73
|
-
|
|
74
|
-
expect(readRecords.length).toBe(2);
|
|
75
|
-
expect(readRecords[0].name).toBe("John Doe");
|
|
76
|
-
expect(readRecords[0].age).toBe(30);
|
|
77
|
-
expect(readRecords[0].email).toBe("john@test.com");
|
|
78
|
-
expect(readRecords[0].active).toBe(true);
|
|
79
|
-
expect(readRecords[1].name).toBe("Jane Smith");
|
|
80
|
-
expect(readRecords[1].age).toBe(25);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("can specify worksheet by index", async () => {
|
|
84
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
85
|
-
|
|
86
|
-
const records = [{ name: "Test", age: 20 }];
|
|
87
|
-
const wb = await wrapper.write("Sheet1", records);
|
|
88
|
-
const buffer = await wb.toBytes();
|
|
89
|
-
await wb.close();
|
|
90
|
-
|
|
91
|
-
const readRecords = await wrapper.read(buffer, 0);
|
|
92
|
-
expect(readRecords.length).toBe(1);
|
|
93
|
-
expect(readRecords[0].name).toBe("Test");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("Type conversion", () => {
|
|
98
|
-
it("applies default values", async () => {
|
|
99
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
100
|
-
|
|
101
|
-
// Save without active field
|
|
102
|
-
const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
|
|
103
|
-
const buffer = await wb.toBytes();
|
|
104
|
-
await wb.close();
|
|
105
|
-
|
|
106
|
-
const records = await wrapper.read(buffer);
|
|
107
|
-
expect(records[0].active).toBe(false); // Default value
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe("Date type support", () => {
|
|
112
|
-
const dateSchema = z.object({
|
|
113
|
-
title: z.string().describe("Title"),
|
|
114
|
-
date: z.instanceof(DateOnly).optional().describe("Date"),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("can read and write DateOnly type", async () => {
|
|
118
|
-
const wrapper = new ExcelWrapper(dateSchema);
|
|
119
|
-
|
|
120
|
-
const records = [{ title: "Event 1", date: new DateOnly(2024, 6, 15) }, { title: "Event 2" }];
|
|
121
|
-
|
|
122
|
-
const wb = await wrapper.write("Events", records);
|
|
123
|
-
const buffer = await wb.toBytes();
|
|
124
|
-
await wb.close();
|
|
125
|
-
|
|
126
|
-
const readRecords = await wrapper.read(buffer, "Events");
|
|
127
|
-
|
|
128
|
-
expect(readRecords[0].title).toBe("Event 1");
|
|
129
|
-
expect(readRecords[0].date).toBeInstanceOf(DateOnly);
|
|
130
|
-
expect(readRecords[0].date!.year).toBe(2024);
|
|
131
|
-
expect(readRecords[0].date!.month).toBe(6);
|
|
132
|
-
expect(readRecords[0].date!.day).toBe(15);
|
|
133
|
-
expect(readRecords[1].date).toBeUndefined();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("can read and write DateTime type", async () => {
|
|
137
|
-
const dateTimeSchema = z.object({
|
|
138
|
-
title: z.string().describe("Title"),
|
|
139
|
-
datetime: z.instanceof(DateTime).optional().describe("DateTime"),
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const wrapper = new ExcelWrapper(dateTimeSchema);
|
|
143
|
-
|
|
144
|
-
const records = [{ title: "Meeting", datetime: new DateTime(2024, 6, 15, 14, 30, 0) }];
|
|
145
|
-
|
|
146
|
-
const wb = await wrapper.write("Events", records);
|
|
147
|
-
const buffer = await wb.toBytes();
|
|
148
|
-
await wb.close();
|
|
149
|
-
|
|
150
|
-
const readRecords = await wrapper.read(buffer, "Events");
|
|
151
|
-
|
|
152
|
-
expect(readRecords[0].datetime).toBeInstanceOf(DateTime);
|
|
153
|
-
expect(readRecords[0].datetime!.year).toBe(2024);
|
|
154
|
-
expect(readRecords[0].datetime!.month).toBe(6);
|
|
155
|
-
expect(readRecords[0].datetime!.day).toBe(15);
|
|
156
|
-
expect(readRecords[0].datetime!.hour).toBe(14);
|
|
157
|
-
expect(readRecords[0].datetime!.minute).toBe(30);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("can read and write Time type", async () => {
|
|
161
|
-
const timeSchema = z.object({
|
|
162
|
-
title: z.string().describe("Title"),
|
|
163
|
-
time: z.instanceof(Time).optional().describe("Time"),
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const wrapper = new ExcelWrapper(timeSchema);
|
|
167
|
-
|
|
168
|
-
const records = [{ title: "Alarm", time: new Time(9, 30, 0) }];
|
|
169
|
-
|
|
170
|
-
const wb = await wrapper.write("Events", records);
|
|
171
|
-
const buffer = await wb.toBytes();
|
|
172
|
-
await wb.close();
|
|
173
|
-
|
|
174
|
-
const readRecords = await wrapper.read(buffer, "Events");
|
|
175
|
-
|
|
176
|
-
expect(readRecords[0].time).toBeInstanceOf(Time);
|
|
177
|
-
expect(readRecords[0].time!.hour).toBe(9);
|
|
178
|
-
expect(readRecords[0].time!.minute).toBe(30);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("Error handling", () => {
|
|
183
|
-
it("throws error when reading empty data", async () => {
|
|
184
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
185
|
-
|
|
186
|
-
// Create empty Excel with only headers
|
|
187
|
-
const wb = await wrapper.write("Empty", []);
|
|
188
|
-
const buffer = await wb.toBytes();
|
|
189
|
-
await wb.close();
|
|
190
|
-
|
|
191
|
-
await expect(wrapper.read(buffer, "Empty")).rejects.toThrow(
|
|
192
|
-
"No data found in Excel file",
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("throws error when reading with non-existent worksheet name", async () => {
|
|
197
|
-
const wrapper = new ExcelWrapper(testSchema);
|
|
198
|
-
|
|
199
|
-
const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
|
|
200
|
-
const buffer = await wb.toBytes();
|
|
201
|
-
await wb.close();
|
|
202
|
-
|
|
203
|
-
await expect(wrapper.read(buffer, "NotExist")).rejects.toThrow();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("throws error with worksheet name and detailed error when schema validation fails", async () => {
|
|
207
|
-
const strictSchema = z.object({
|
|
208
|
-
name: z.string().min(5).describe("이름"), // At least 5 characters
|
|
209
|
-
age: z.number().min(0).max(150).describe("나이"), // Between 0 and 150
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Create Excel with valid data
|
|
213
|
-
const wrapper = new ExcelWrapper(strictSchema);
|
|
214
|
-
const wb = await wrapper.write("Validation", [{ name: "홍길동홍길동", age: 30 }]);
|
|
215
|
-
|
|
216
|
-
// Modify data directly to trigger validation failure
|
|
217
|
-
const ws = await wb.getWorksheet("Validation");
|
|
218
|
-
await ws.cell(1, 0).setValue("AB"); // Change to less than 5 characters
|
|
219
|
-
|
|
220
|
-
const buffer = await wb.toBytes();
|
|
221
|
-
await wb.close();
|
|
222
|
-
|
|
223
|
-
// Should throw validation error
|
|
224
|
-
await expect(wrapper.read(buffer, "Validation")).rejects.toThrow();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe("excludes option", () => {
|
|
229
|
-
const fullSchema = z.object({
|
|
230
|
-
name: z.string().describe("Name"),
|
|
231
|
-
age: z.number().describe("Age"),
|
|
232
|
-
email: z.string().optional().describe("Email"),
|
|
233
|
-
phone: z.string().optional().describe("Phone"),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("excludes specified fields from columns on write", async () => {
|
|
237
|
-
const wrapper = new ExcelWrapper(fullSchema);
|
|
238
|
-
|
|
239
|
-
const records = [{ name: "John Doe", age: 30, email: "john@test.com", phone: "010-1234-5678" }];
|
|
240
|
-
const wb = await wrapper.write("Test", records, { excludes: ["email", "phone"] });
|
|
241
|
-
const ws = await wb.getWorksheet("Test");
|
|
242
|
-
|
|
243
|
-
// Headers: only name and age exist
|
|
244
|
-
expect(await ws.cell(0, 0).getValue()).toBe("Name");
|
|
245
|
-
expect(await ws.cell(0, 1).getValue()).toBe("Age");
|
|
246
|
-
expect(await ws.cell(0, 2).getValue()).toBeUndefined();
|
|
247
|
-
|
|
248
|
-
// Check data
|
|
249
|
-
expect(await ws.cell(1, 0).getValue()).toBe("John Doe");
|
|
250
|
-
expect(await ws.cell(1, 1).getValue()).toBe(30);
|
|
251
|
-
|
|
252
|
-
await wb.close();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("ignores excluded fields on read", async () => {
|
|
256
|
-
const wrapper = new ExcelWrapper(fullSchema);
|
|
257
|
-
|
|
258
|
-
// Create Excel with all fields
|
|
259
|
-
const records = [{ name: "John Doe", age: 30, email: "john@test.com", phone: "010-1234-5678" }];
|
|
260
|
-
const wb = await wrapper.write("Test", records);
|
|
261
|
-
const buffer = await wb.toBytes();
|
|
262
|
-
await wb.close();
|
|
263
|
-
|
|
264
|
-
// Read with excludes
|
|
265
|
-
const readRecords = await wrapper.read(buffer, "Test", { excludes: ["email", "phone"] });
|
|
266
|
-
|
|
267
|
-
expect(readRecords[0].name).toBe("John Doe");
|
|
268
|
-
expect(readRecords[0].age).toBe(30);
|
|
269
|
-
expect(readRecords[0].email).toBeUndefined();
|
|
270
|
-
expect(readRecords[0].phone).toBeUndefined();
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
});
|
package/tests/fixtures/logo.png
DELETED
|
Binary file
|
|
Binary file
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { ExcelWorkbook } from "../src/excel-workbook";
|
|
3
|
-
|
|
4
|
-
// If globalThis.window doesn't exist, it's a Node.js environment
|
|
5
|
-
declare const window: unknown;
|
|
6
|
-
// Node.js only types (to pass type checking in browser environment as well)
|
|
7
|
-
declare const require: (id: string) => unknown;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Load PNG file (branching between Node/browser environments)
|
|
11
|
-
*/
|
|
12
|
-
async function loadPngFile(): Promise<Uint8Array> {
|
|
13
|
-
const url = new URL("./fixtures/logo.png", import.meta.url);
|
|
14
|
-
|
|
15
|
-
// Node environment: use fs
|
|
16
|
-
if (typeof window === "undefined") {
|
|
17
|
-
const fs = require("fs") as { readFileSync: (path: string) => Uint8Array };
|
|
18
|
-
const { fileURLToPath } = require("url") as { fileURLToPath: (url: URL) => string };
|
|
19
|
-
const filePath = fileURLToPath(url);
|
|
20
|
-
return new Uint8Array(fs.readFileSync(filePath));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Browser environment: use fetch
|
|
24
|
-
const response = await fetch(url);
|
|
25
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
26
|
-
return new Uint8Array(arrayBuffer);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Integration test: verify ExcelWorksheet.addImage behavior
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
describe("ExcelWorksheet.addImage integration", () => {
|
|
34
|
-
it("should write media, drawing, drawing rels, worksheet rel and content types", async () => {
|
|
35
|
-
const wb = new ExcelWorkbook();
|
|
36
|
-
const ws = await wb.addWorksheet("Sheet1");
|
|
37
|
-
|
|
38
|
-
const bytes = await loadPngFile();
|
|
39
|
-
|
|
40
|
-
// call addImage (the single entry API)
|
|
41
|
-
await ws.addImage({
|
|
42
|
-
bytes,
|
|
43
|
-
ext: "png",
|
|
44
|
-
from: { r: 0, c: 0 },
|
|
45
|
-
to: { r: 2, c: 2 },
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// --- 1) Check media exists and its content
|
|
49
|
-
const mediaPath = `xl/media/image1.png`;
|
|
50
|
-
const mediaObj = await (ws as any)._zipCache.get(mediaPath);
|
|
51
|
-
expect(mediaObj).toBeDefined();
|
|
52
|
-
expect(mediaObj instanceof Uint8Array).toBe(true);
|
|
53
|
-
expect(mediaObj).toEqual(bytes);
|
|
54
|
-
|
|
55
|
-
// --- 2) Check Content Types has media / drawing override
|
|
56
|
-
const types = await (ws as any)._zipCache.get("[Content_Types].xml");
|
|
57
|
-
expect(types).toBeDefined();
|
|
58
|
-
// types is likely an ExcelXmlContentType instance
|
|
59
|
-
const overrides = types.data?.Types?.Override ?? [];
|
|
60
|
-
expect(overrides.some((o: any) => o.$.PartName === "/xl/media/image1.png")).toBeTruthy();
|
|
61
|
-
expect(overrides.some((o: any) => o.$.PartName === "/xl/drawings/drawing1.xml")).toBeTruthy();
|
|
62
|
-
|
|
63
|
-
// --- 3) Check drawing xml exists and a:blip r:embed is set as rId
|
|
64
|
-
const drawingPath = "xl/drawings/drawing1.xml";
|
|
65
|
-
const drawingObj = await (ws as any)._zipCache.get(drawingPath);
|
|
66
|
-
expect(drawingObj).toBeDefined();
|
|
67
|
-
|
|
68
|
-
// drawingObj is likely an ExcelXmlDrawing object — check internal structure
|
|
69
|
-
const wsDr = drawingObj?.data?.wsDr;
|
|
70
|
-
expect(wsDr).toBeDefined();
|
|
71
|
-
const anchors = wsDr.twoCellAnchor ?? [];
|
|
72
|
-
expect(anchors.length).toBeGreaterThan(0);
|
|
73
|
-
const pic = anchors[0].pic?.[0];
|
|
74
|
-
expect(pic).toBeDefined();
|
|
75
|
-
const aBlip = pic?.blipFill?.[0]?.["a:blip"]?.[0];
|
|
76
|
-
expect(aBlip).toBeDefined();
|
|
77
|
-
const embedVal = aBlip?.$?.["r:embed"];
|
|
78
|
-
expect(typeof embedVal === "string" && /^rId\d+$/.test(embedVal)).toBeTruthy();
|
|
79
|
-
|
|
80
|
-
// --- 4) Check drawing rels exists and references media target
|
|
81
|
-
const drawingRels = await (ws as any)._zipCache.get("xl/drawings/_rels/drawing1.xml.rels");
|
|
82
|
-
expect(drawingRels).toBeDefined();
|
|
83
|
-
const relsArr = drawingRels?.data?.Relationships?.Relationship ?? [];
|
|
84
|
-
expect(relsArr.some((r: any) => r.$.Target === "../media/image1.png")).toBeTruthy();
|
|
85
|
-
|
|
86
|
-
// --- 5) Check worksheet rels has drawing rel added and worksheet xml has <drawing r:id="..."/>
|
|
87
|
-
const sheetFileName = (ws as any)._targetFileName; // e.g., "sheet1.xml"
|
|
88
|
-
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
89
|
-
const sheetRels = await (ws as any)._zipCache.get(sheetRelsPath);
|
|
90
|
-
expect(sheetRels).toBeDefined();
|
|
91
|
-
const sheetRelsArr = sheetRels?.data?.Relationships?.Relationship ?? [];
|
|
92
|
-
expect(
|
|
93
|
-
sheetRelsArr.some(
|
|
94
|
-
(r: any) => r.$.Target != null && r.$.Target.indexOf("/drawings/drawing") !== -1,
|
|
95
|
-
),
|
|
96
|
-
).toBeTruthy();
|
|
97
|
-
|
|
98
|
-
const wsXml = await (ws as any)._zipCache.get(`xl/worksheets/${sheetFileName}`);
|
|
99
|
-
expect(wsXml).toBeDefined();
|
|
100
|
-
expect(Array.isArray(wsXml.data?.worksheet?.drawing)).toBeTruthy();
|
|
101
|
-
const drawingElems = wsXml.data.worksheet.drawing;
|
|
102
|
-
expect(drawingElems.some((d: any) => d.$ != null && d.$["r:id"] != null)).toBeTruthy();
|
|
103
|
-
|
|
104
|
-
// Verify buffer creation
|
|
105
|
-
const resultBuffer = await wb.toBytes();
|
|
106
|
-
expect(resultBuffer).toBeDefined();
|
|
107
|
-
expect(resultBuffer.length).toBeGreaterThan(0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("can insert multiple images into the same worksheet", async () => {
|
|
111
|
-
const wb = new ExcelWorkbook();
|
|
112
|
-
const ws = await wb.addWorksheet("Sheet1");
|
|
113
|
-
|
|
114
|
-
const bytes = await loadPngFile();
|
|
115
|
-
|
|
116
|
-
// Insert first image
|
|
117
|
-
await ws.addImage({
|
|
118
|
-
bytes,
|
|
119
|
-
ext: "png",
|
|
120
|
-
from: { r: 0, c: 0 },
|
|
121
|
-
to: { r: 2, c: 2 },
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Insert second image (different location)
|
|
125
|
-
await ws.addImage({
|
|
126
|
-
bytes,
|
|
127
|
-
ext: "png",
|
|
128
|
-
from: { r: 3, c: 0 },
|
|
129
|
-
to: { r: 5, c: 2 },
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// --- 1) Check that two media files were created
|
|
133
|
-
const media1 = await (ws as any)._zipCache.get("xl/media/image1.png");
|
|
134
|
-
const media2 = await (ws as any)._zipCache.get("xl/media/image2.png");
|
|
135
|
-
expect(media1).toBeDefined();
|
|
136
|
-
expect(media2).toBeDefined();
|
|
137
|
-
expect(media1 instanceof Uint8Array).toBe(true);
|
|
138
|
-
expect(media2 instanceof Uint8Array).toBe(true);
|
|
139
|
-
|
|
140
|
-
// --- 2) Check that both images are registered in Content Types
|
|
141
|
-
const types = await (ws as any)._zipCache.get("[Content_Types].xml");
|
|
142
|
-
const overrides = types.data?.Types?.Override ?? [];
|
|
143
|
-
expect(overrides.some((o: any) => o.$.PartName === "/xl/media/image1.png")).toBeTruthy();
|
|
144
|
-
expect(overrides.some((o: any) => o.$.PartName === "/xl/media/image2.png")).toBeTruthy();
|
|
145
|
-
|
|
146
|
-
// --- 3) Check that drawing xml has two anchors
|
|
147
|
-
const drawingObj = await (ws as any)._zipCache.get("xl/drawings/drawing1.xml");
|
|
148
|
-
const anchors = drawingObj?.data?.wsDr?.twoCellAnchor ?? [];
|
|
149
|
-
expect(anchors.length).toBe(2);
|
|
150
|
-
|
|
151
|
-
// First image anchor
|
|
152
|
-
const pic1 = anchors[0].pic?.[0];
|
|
153
|
-
expect(pic1).toBeDefined();
|
|
154
|
-
const embed1 = pic1?.blipFill?.[0]?.["a:blip"]?.[0]?.$?.["r:embed"];
|
|
155
|
-
expect(embed1).toBeDefined();
|
|
156
|
-
|
|
157
|
-
// Second image anchor
|
|
158
|
-
const pic2 = anchors[1].pic?.[0];
|
|
159
|
-
expect(pic2).toBeDefined();
|
|
160
|
-
const embed2 = pic2?.blipFill?.[0]?.["a:blip"]?.[0]?.$?.["r:embed"];
|
|
161
|
-
expect(embed2).toBeDefined();
|
|
162
|
-
|
|
163
|
-
// Check that rIds are different
|
|
164
|
-
expect(embed1).not.toBe(embed2);
|
|
165
|
-
|
|
166
|
-
// --- 4) Check that drawing rels have relationships for both images
|
|
167
|
-
const drawingRels = await (ws as any)._zipCache.get("xl/drawings/_rels/drawing1.xml.rels");
|
|
168
|
-
const relsArr = drawingRels?.data?.Relationships?.Relationship ?? [];
|
|
169
|
-
expect(relsArr.some((r: any) => r.$.Target === "../media/image1.png")).toBeTruthy();
|
|
170
|
-
expect(relsArr.some((r: any) => r.$.Target === "../media/image2.png")).toBeTruthy();
|
|
171
|
-
|
|
172
|
-
// Verify buffer creation
|
|
173
|
-
const resultBuffer = await wb.toBytes();
|
|
174
|
-
expect(resultBuffer).toBeDefined();
|
|
175
|
-
expect(resultBuffer.length).toBeGreaterThan(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("throws error for unsupported file extensions", async () => {
|
|
179
|
-
const wb = new ExcelWorkbook();
|
|
180
|
-
const ws = await wb.addWorksheet("Sheet1");
|
|
181
|
-
|
|
182
|
-
await expect(
|
|
183
|
-
ws.addImage({
|
|
184
|
-
bytes: new Uint8Array([0, 1, 2, 3]),
|
|
185
|
-
ext: "xyz123",
|
|
186
|
-
from: { r: 0, c: 0 },
|
|
187
|
-
}),
|
|
188
|
-
).rejects.toThrow("Cannot determine MIME type");
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { ExcelUtils } from "../../src/utils/excel-utils";
|
|
3
|
-
|
|
4
|
-
describe("ExcelUtils", () => {
|
|
5
|
-
describe("stringifyColAddr / parseColAddr", () => {
|
|
6
|
-
it("converts 26 and above to AA, AB, etc.", () => {
|
|
7
|
-
expect(ExcelUtils.stringifyColAddr(26)).toBe("AA");
|
|
8
|
-
expect(ExcelUtils.stringifyColAddr(27)).toBe("AB");
|
|
9
|
-
expect(ExcelUtils.stringifyColAddr(51)).toBe("AZ");
|
|
10
|
-
expect(ExcelUtils.stringifyColAddr(52)).toBe("BA");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("handles large column indices", () => {
|
|
14
|
-
// 702 = AAA (26^2 + 26 = 702)
|
|
15
|
-
expect(ExcelUtils.stringifyColAddr(702)).toBe("AAA");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("handles Excel maximum column index (XFD, 16383)", () => {
|
|
19
|
-
// Excel maximum column is XFD (index 16383, 0-based)
|
|
20
|
-
expect(ExcelUtils.stringifyColAddr(16383)).toBe("XFD");
|
|
21
|
-
expect(ExcelUtils.parseColAddr("XFD")).toBe(16383);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("round-trip: stringify → parse returns original value", () => {
|
|
25
|
-
for (let i = 0; i < 100; i++) {
|
|
26
|
-
const stringified = ExcelUtils.stringifyColAddr(i);
|
|
27
|
-
const parsed = ExcelUtils.parseColAddr(stringified);
|
|
28
|
-
expect(parsed).toBe(i);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("throws error on negative column index input", () => {
|
|
33
|
-
expect(() => ExcelUtils.stringifyColAddr(-1)).toThrow();
|
|
34
|
-
expect(() => ExcelUtils.stringifyColAddr(-100)).toThrow();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("stringifyRowAddr / parseRowAddr", () => {
|
|
39
|
-
it("converts 0-based index to 1-based string", () => {
|
|
40
|
-
expect(ExcelUtils.stringifyRowAddr(0)).toBe("1");
|
|
41
|
-
expect(ExcelUtils.stringifyRowAddr(9)).toBe("10");
|
|
42
|
-
expect(ExcelUtils.stringifyRowAddr(99)).toBe("100");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("parses 1-based string to 0-based index", () => {
|
|
46
|
-
expect(ExcelUtils.parseRowAddr("A1")).toBe(0);
|
|
47
|
-
expect(ExcelUtils.parseRowAddr("B10")).toBe(9);
|
|
48
|
-
expect(ExcelUtils.parseRowAddr("AA100")).toBe(99);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("stringifyAddr / parseCellAddr", () => {
|
|
53
|
-
it("correctly converts cell address", () => {
|
|
54
|
-
expect(ExcelUtils.stringifyAddr({ r: 0, c: 0 })).toBe("A1");
|
|
55
|
-
expect(ExcelUtils.stringifyAddr({ r: 9, c: 1 })).toBe("B10");
|
|
56
|
-
expect(ExcelUtils.stringifyAddr({ r: 99, c: 26 })).toBe("AA100");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("correctly parses cell address", () => {
|
|
60
|
-
expect(ExcelUtils.parseCellAddr("A1")).toEqual({ r: 0, c: 0 });
|
|
61
|
-
expect(ExcelUtils.parseCellAddr("B10")).toEqual({ r: 9, c: 1 });
|
|
62
|
-
expect(ExcelUtils.parseCellAddr("AA100")).toEqual({ r: 99, c: 26 });
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("parseRangeAddr / stringifyRangeAddr", () => {
|
|
67
|
-
it("parses single cell range", () => {
|
|
68
|
-
const range = ExcelUtils.parseRangeAddr("A1");
|
|
69
|
-
expect(range.s).toEqual({ r: 0, c: 0 });
|
|
70
|
-
expect(range.e).toEqual({ r: 0, c: 0 });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("parses multi-cell range", () => {
|
|
74
|
-
const range = ExcelUtils.parseRangeAddr("A1:C3");
|
|
75
|
-
expect(range.s).toEqual({ r: 0, c: 0 });
|
|
76
|
-
expect(range.e).toEqual({ r: 2, c: 2 });
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("converts single cell range to string", () => {
|
|
80
|
-
const addr = ExcelUtils.stringifyRangeAddr({
|
|
81
|
-
s: { r: 0, c: 0 },
|
|
82
|
-
e: { r: 0, c: 0 },
|
|
83
|
-
});
|
|
84
|
-
expect(addr).toBe("A1");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("converts multi-cell range to string", () => {
|
|
88
|
-
const addr = ExcelUtils.stringifyRangeAddr({
|
|
89
|
-
s: { r: 0, c: 0 },
|
|
90
|
-
e: { r: 2, c: 2 },
|
|
91
|
-
});
|
|
92
|
-
expect(addr).toBe("A1:C3");
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("convertTimeTickToNumber / convertNumberToTimeTick", () => {
|
|
97
|
-
it("correctly converts 1970-01-01", () => {
|
|
98
|
-
const date = new Date(Date.UTC(1970, 0, 1, 0, 0, 0));
|
|
99
|
-
const tick = date.getTime();
|
|
100
|
-
const excelNum = ExcelUtils.convertTimeTickToNumber(tick);
|
|
101
|
-
// 1970-01-01 is 25569 days in Excel's date system
|
|
102
|
-
expect(excelNum).toBeCloseTo(25569, 0);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("round-trip: tick → number → tick returns original value", () => {
|
|
106
|
-
const originalDate = new Date(Date.UTC(2024, 5, 15, 14, 30, 45));
|
|
107
|
-
const tick = originalDate.getTime();
|
|
108
|
-
const excelNum = ExcelUtils.convertTimeTickToNumber(tick);
|
|
109
|
-
const recoveredTick = ExcelUtils.convertNumberToTimeTick(excelNum);
|
|
110
|
-
// May not match exactly to milliseconds, so compare at second level
|
|
111
|
-
expect(Math.floor(recoveredTick / 1000)).toBe(Math.floor(tick / 1000));
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("convertNumFmtCodeToName", () => {
|
|
116
|
-
it("recognizes General as number", () => {
|
|
117
|
-
expect(ExcelUtils.convertNumFmtCodeToName("General")).toBe("number");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("recognizes date pattern as DateOnly", () => {
|
|
121
|
-
expect(ExcelUtils.convertNumFmtCodeToName("yyyy-mm-dd")).toBe("DateOnly");
|
|
122
|
-
expect(ExcelUtils.convertNumFmtCodeToName("yy/mm/dd")).toBe("DateOnly");
|
|
123
|
-
expect(ExcelUtils.convertNumFmtCodeToName("dd-mmm-yyyy")).toBe("DateOnly");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// NOTE: In the current implementation, 'mm' is used in both date (month) and time (minute)
|
|
127
|
-
// Regex: hasDate = /yy|dd|mm/i, hasTime = /hh|ss/i
|
|
128
|
-
// Therefore "hh:mm:ss" has hasDate(mm)=true, hasTime(hh,ss)=true → DateTime
|
|
129
|
-
// "h:mm" has hasDate(mm)=true, hasTime=false (h≠hh) → DateOnly
|
|
130
|
-
it("handles time pattern (note mm ambiguity)", () => {
|
|
131
|
-
// "hh:mm:ss": hasDate(mm)=true, hasTime(hh,ss)=true → DateTime
|
|
132
|
-
expect(ExcelUtils.convertNumFmtCodeToName("hh:mm:ss")).toBe("DateTime");
|
|
133
|
-
// "h:mm": hasDate(mm)=true, hasTime=false (h≠hh) → DateOnly
|
|
134
|
-
expect(ExcelUtils.convertNumFmtCodeToName("h:mm")).toBe("DateOnly");
|
|
135
|
-
// Pure time format (only ss, no mm)
|
|
136
|
-
expect(ExcelUtils.convertNumFmtCodeToName("[h]:ss")).toBe("Time");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("recognizes date+time pattern as DateTime", () => {
|
|
140
|
-
expect(ExcelUtils.convertNumFmtCodeToName("yyyy-mm-dd hh:mm:ss")).toBe("DateTime");
|
|
141
|
-
// "yy/mm/dd h:mm" detects only date (h:mm has no hh and no ss)
|
|
142
|
-
expect(ExcelUtils.convertNumFmtCodeToName("yy/mm/dd h:mm")).toBe("DateOnly");
|
|
143
|
-
// Clear DateTime pattern
|
|
144
|
-
expect(ExcelUtils.convertNumFmtCodeToName("yyyy/mm/dd hh:mm:ss")).toBe("DateTime");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("recognizes number pattern as number", () => {
|
|
148
|
-
expect(ExcelUtils.convertNumFmtCodeToName("#,##0")).toBe("number");
|
|
149
|
-
expect(ExcelUtils.convertNumFmtCodeToName("0.00")).toBe("number");
|
|
150
|
-
expect(ExcelUtils.convertNumFmtCodeToName("#,0")).toBe("number");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("throws error on unknown format code input", () => {
|
|
154
|
-
expect(() => ExcelUtils.convertNumFmtCodeToName("unknown-format-xyz")).toThrow();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("convertNumFmtIdToName", () => {
|
|
159
|
-
it("recognizes common number formats", () => {
|
|
160
|
-
expect(ExcelUtils.convertNumFmtIdToName(0)).toBe("number");
|
|
161
|
-
expect(ExcelUtils.convertNumFmtIdToName(1)).toBe("number");
|
|
162
|
-
expect(ExcelUtils.convertNumFmtIdToName(2)).toBe("number");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("recognizes date format", () => {
|
|
166
|
-
expect(ExcelUtils.convertNumFmtIdToName(14)).toBe("DateOnly");
|
|
167
|
-
expect(ExcelUtils.convertNumFmtIdToName(15)).toBe("DateOnly");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("recognizes date+time format", () => {
|
|
171
|
-
expect(ExcelUtils.convertNumFmtIdToName(22)).toBe("DateTime");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("recognizes time format", () => {
|
|
175
|
-
expect(ExcelUtils.convertNumFmtIdToName(18)).toBe("Time");
|
|
176
|
-
expect(ExcelUtils.convertNumFmtIdToName(19)).toBe("Time");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("recognizes text format", () => {
|
|
180
|
-
expect(ExcelUtils.convertNumFmtIdToName(49)).toBe("string");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("throws error on unknown numFmtId input", () => {
|
|
184
|
-
expect(() => ExcelUtils.convertNumFmtIdToName(23)).toThrow();
|
|
185
|
-
expect(() => ExcelUtils.convertNumFmtIdToName(100)).toThrow();
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe("convertNumFmtNameToId", () => {
|
|
190
|
-
it("converts format name to ID", () => {
|
|
191
|
-
expect(ExcelUtils.convertNumFmtNameToId("number")).toBe(0);
|
|
192
|
-
expect(ExcelUtils.convertNumFmtNameToId("DateOnly")).toBe(14);
|
|
193
|
-
expect(ExcelUtils.convertNumFmtNameToId("DateTime")).toBe(22);
|
|
194
|
-
expect(ExcelUtils.convertNumFmtNameToId("Time")).toBe(18);
|
|
195
|
-
expect(ExcelUtils.convertNumFmtNameToId("string")).toBe(49);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
});
|