@simplysm/excel 13.0.69 → 13.0.70

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 (74) hide show
  1. package/README.md +19 -516
  2. package/dist/excel-cell.d.ts +28 -28
  3. package/dist/excel-cell.d.ts.map +1 -1
  4. package/dist/excel-cell.js +24 -24
  5. package/dist/excel-cell.js.map +1 -1
  6. package/dist/excel-col.d.ts +4 -4
  7. package/dist/excel-col.d.ts.map +1 -1
  8. package/dist/excel-col.js +3 -3
  9. package/dist/excel-row.d.ts +3 -3
  10. package/dist/excel-row.d.ts.map +1 -1
  11. package/dist/excel-row.js +2 -2
  12. package/dist/excel-workbook.d.ts +23 -23
  13. package/dist/excel-workbook.d.ts.map +1 -1
  14. package/dist/excel-workbook.js +15 -15
  15. package/dist/excel-workbook.js.map +1 -1
  16. package/dist/excel-worksheet.d.ts +32 -32
  17. package/dist/excel-worksheet.d.ts.map +1 -1
  18. package/dist/excel-worksheet.js +32 -32
  19. package/dist/excel-worksheet.js.map +1 -1
  20. package/dist/excel-wrapper.d.ts +7 -7
  21. package/dist/excel-wrapper.js +7 -7
  22. package/dist/excel-wrapper.js.map +1 -1
  23. package/dist/types.d.ts +16 -16
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils/excel-utils.d.ts +23 -23
  26. package/dist/utils/excel-utils.d.ts.map +1 -1
  27. package/dist/utils/excel-utils.js +25 -25
  28. package/dist/utils/excel-utils.js.map +1 -1
  29. package/dist/utils/zip-cache.d.ts +6 -6
  30. package/dist/xml/excel-xml-content-type.d.ts +2 -2
  31. package/dist/xml/excel-xml-drawing.d.ts +2 -2
  32. package/dist/xml/excel-xml-relationship.d.ts +2 -2
  33. package/dist/xml/excel-xml-relationship.js +1 -1
  34. package/dist/xml/excel-xml-relationship.js.map +1 -1
  35. package/dist/xml/excel-xml-shared-string.d.ts +2 -2
  36. package/dist/xml/excel-xml-style.d.ts +2 -2
  37. package/dist/xml/excel-xml-style.js +6 -6
  38. package/dist/xml/excel-xml-style.js.map +1 -1
  39. package/dist/xml/excel-xml-unknown.d.ts +2 -2
  40. package/dist/xml/excel-xml-workbook.d.ts +2 -2
  41. package/dist/xml/excel-xml-workbook.js +1 -1
  42. package/dist/xml/excel-xml-workbook.js.map +1 -1
  43. package/dist/xml/excel-xml-worksheet.d.ts +6 -6
  44. package/dist/xml/excel-xml-worksheet.d.ts.map +1 -1
  45. package/dist/xml/excel-xml-worksheet.js +9 -8
  46. package/dist/xml/excel-xml-worksheet.js.map +1 -1
  47. package/package.json +6 -5
  48. package/src/excel-cell.ts +35 -35
  49. package/src/excel-col.ts +4 -4
  50. package/src/excel-row.ts +3 -3
  51. package/src/excel-workbook.ts +30 -30
  52. package/src/excel-worksheet.ts +47 -47
  53. package/src/excel-wrapper.ts +18 -18
  54. package/src/index.ts +3 -3
  55. package/src/types.ts +15 -17
  56. package/src/utils/excel-utils.ts +38 -38
  57. package/src/utils/zip-cache.ts +6 -6
  58. package/src/xml/excel-xml-content-type.ts +3 -3
  59. package/src/xml/excel-xml-drawing.ts +2 -2
  60. package/src/xml/excel-xml-relationship.ts +3 -3
  61. package/src/xml/excel-xml-shared-string.ts +2 -2
  62. package/src/xml/excel-xml-style.ts +11 -11
  63. package/src/xml/excel-xml-unknown.ts +2 -2
  64. package/src/xml/excel-xml-workbook.ts +5 -5
  65. package/src/xml/excel-xml-worksheet.ts +44 -43
  66. package/tests/excel-cell.spec.ts +448 -0
  67. package/tests/excel-col.spec.ts +112 -0
  68. package/tests/excel-row.spec.ts +71 -0
  69. package/tests/excel-workbook.spec.ts +160 -0
  70. package/tests/excel-worksheet.spec.ts +389 -0
  71. package/tests/excel-wrapper.spec.ts +296 -0
  72. package/tests/fixtures/logo.png +0 -0
  73. package/tests/image-insert.spec.ts +190 -0
  74. package/tests/utils/excel-utils.spec.ts +242 -0
@@ -0,0 +1,296 @@
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).getVal()).toBe("Name");
28
+ expect(await ws.cell(0, 1).getVal()).toBe("Age");
29
+ expect(await ws.cell(0, 2).getVal()).toBe("Email");
30
+ expect(await ws.cell(0, 3).getVal()).toBe("Active");
31
+
32
+ // Check data
33
+ expect(await ws.cell(1, 0).getVal()).toBe("John Doe");
34
+ expect(await ws.cell(1, 1).getVal()).toBe(30);
35
+ expect(await ws.cell(2, 0).getVal()).toBe("Jane Smith");
36
+ expect(await ws.cell(2, 1).getVal()).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.getBytes();
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.getBytes();
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("can convert strings to numbers", async () => {
99
+ const wrapper = new ExcelWrapper(testSchema);
100
+
101
+ // Simulate Excel with values stored as strings manually
102
+ const wb = await wrapper.write("Test", [{ name: "Test", age: 25 }]);
103
+ const buffer = await wb.getBytes();
104
+ await wb.close();
105
+
106
+ const records = await wrapper.read(buffer);
107
+ expect(typeof records[0].age).toBe("number");
108
+ expect(records[0].age).toBe(25);
109
+ });
110
+
111
+ it("applies default values", async () => {
112
+ const wrapper = new ExcelWrapper(testSchema);
113
+
114
+ // Save without active field
115
+ const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
116
+ const buffer = await wb.getBytes();
117
+ await wb.close();
118
+
119
+ const records = await wrapper.read(buffer);
120
+ expect(records[0].active).toBe(false); // Default value
121
+ });
122
+ });
123
+
124
+ describe("Date type support", () => {
125
+ const dateSchema = z.object({
126
+ title: z.string().describe("Title"),
127
+ date: z.instanceof(DateOnly).optional().describe("Date"),
128
+ });
129
+
130
+ it("can read and write DateOnly type", async () => {
131
+ const wrapper = new ExcelWrapper(dateSchema);
132
+
133
+ const records = [{ title: "Event 1", date: new DateOnly(2024, 6, 15) }, { title: "Event 2" }];
134
+
135
+ const wb = await wrapper.write("Events", records);
136
+ const buffer = await wb.getBytes();
137
+ await wb.close();
138
+
139
+ const readRecords = await wrapper.read(buffer, "Events");
140
+
141
+ expect(readRecords[0].title).toBe("Event 1");
142
+ expect(readRecords[0].date).toBeInstanceOf(DateOnly);
143
+ expect(readRecords[0].date!.year).toBe(2024);
144
+ expect(readRecords[0].date!.month).toBe(6);
145
+ expect(readRecords[0].date!.day).toBe(15);
146
+ expect(readRecords[1].date).toBeUndefined();
147
+ });
148
+
149
+ it("can read and write DateTime type", async () => {
150
+ const dateTimeSchema = z.object({
151
+ title: z.string().describe("Title"),
152
+ datetime: z.instanceof(DateTime).optional().describe("DateTime"),
153
+ });
154
+
155
+ const wrapper = new ExcelWrapper(dateTimeSchema);
156
+
157
+ const records = [{ title: "Meeting", datetime: new DateTime(2024, 6, 15, 14, 30, 0) }];
158
+
159
+ const wb = await wrapper.write("Events", records);
160
+ const buffer = await wb.getBytes();
161
+ await wb.close();
162
+
163
+ const readRecords = await wrapper.read(buffer, "Events");
164
+
165
+ expect(readRecords[0].datetime).toBeInstanceOf(DateTime);
166
+ expect(readRecords[0].datetime!.year).toBe(2024);
167
+ expect(readRecords[0].datetime!.month).toBe(6);
168
+ expect(readRecords[0].datetime!.day).toBe(15);
169
+ expect(readRecords[0].datetime!.hour).toBe(14);
170
+ expect(readRecords[0].datetime!.minute).toBe(30);
171
+ });
172
+
173
+ it("can read and write Time type", async () => {
174
+ const timeSchema = z.object({
175
+ title: z.string().describe("Title"),
176
+ time: z.instanceof(Time).optional().describe("Time"),
177
+ });
178
+
179
+ const wrapper = new ExcelWrapper(timeSchema);
180
+
181
+ const records = [{ title: "Alarm", time: new Time(9, 30, 0) }];
182
+
183
+ const wb = await wrapper.write("Events", records);
184
+ const buffer = await wb.getBytes();
185
+ await wb.close();
186
+
187
+ const readRecords = await wrapper.read(buffer, "Events");
188
+
189
+ expect(readRecords[0].time).toBeInstanceOf(Time);
190
+ expect(readRecords[0].time!.hour).toBe(9);
191
+ expect(readRecords[0].time!.minute).toBe(30);
192
+ });
193
+ });
194
+
195
+ describe("Error handling", () => {
196
+ it("throws error when reading empty data", async () => {
197
+ const wrapper = new ExcelWrapper(testSchema);
198
+
199
+ // Create empty Excel with only headers
200
+ const wb = await wrapper.write("Empty", []);
201
+ const buffer = await wb.getBytes();
202
+ await wb.close();
203
+
204
+ await expect(wrapper.read(buffer, "Empty")).rejects.toThrow(
205
+ "No data found in Excel file",
206
+ );
207
+ });
208
+
209
+ it("throws error when reading with non-existent worksheet name", async () => {
210
+ const wrapper = new ExcelWrapper(testSchema);
211
+
212
+ const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
213
+ const buffer = await wb.getBytes();
214
+ await wb.close();
215
+
216
+ await expect(wrapper.read(buffer, "NotExist")).rejects.toThrow();
217
+ });
218
+
219
+ it("throws error when reading with non-existent worksheet index", async () => {
220
+ const wrapper = new ExcelWrapper(testSchema);
221
+
222
+ const wb = await wrapper.write("Test", [{ name: "Test", age: 20 }]);
223
+ const buffer = await wb.getBytes();
224
+ await wb.close();
225
+
226
+ await expect(wrapper.read(buffer, 99)).rejects.toThrow();
227
+ });
228
+
229
+ it("throws error with worksheet name and detailed error when schema validation fails", async () => {
230
+ const strictSchema = z.object({
231
+ name: z.string().min(5).describe("이름"), // At least 5 characters
232
+ age: z.number().min(0).max(150).describe("나이"), // Between 0 and 150
233
+ });
234
+
235
+ // Create Excel with valid data
236
+ const wrapper = new ExcelWrapper(strictSchema);
237
+ const wb = await wrapper.write("Validation", [{ name: "홍길동홍길동", age: 30 }]);
238
+
239
+ // Modify data directly to trigger validation failure
240
+ const ws = await wb.getWorksheet("Validation");
241
+ await ws.cell(1, 0).setVal("AB"); // Change to less than 5 characters
242
+
243
+ const buffer = await wb.getBytes();
244
+ await wb.close();
245
+
246
+ // Should throw validation error
247
+ await expect(wrapper.read(buffer, "Validation")).rejects.toThrow();
248
+ });
249
+ });
250
+
251
+ describe("excludes option", () => {
252
+ const fullSchema = z.object({
253
+ name: z.string().describe("Name"),
254
+ age: z.number().describe("Age"),
255
+ email: z.string().optional().describe("Email"),
256
+ phone: z.string().optional().describe("Phone"),
257
+ });
258
+
259
+ it("excludes specified fields from columns on write", async () => {
260
+ const wrapper = new ExcelWrapper(fullSchema);
261
+
262
+ const records = [{ name: "John Doe", age: 30, email: "john@test.com", phone: "010-1234-5678" }];
263
+ const wb = await wrapper.write("Test", records, { excludes: ["email", "phone"] });
264
+ const ws = await wb.getWorksheet("Test");
265
+
266
+ // Headers: only name and age exist
267
+ expect(await ws.cell(0, 0).getVal()).toBe("Name");
268
+ expect(await ws.cell(0, 1).getVal()).toBe("Age");
269
+ expect(await ws.cell(0, 2).getVal()).toBeUndefined();
270
+
271
+ // Check data
272
+ expect(await ws.cell(1, 0).getVal()).toBe("John Doe");
273
+ expect(await ws.cell(1, 1).getVal()).toBe(30);
274
+
275
+ await wb.close();
276
+ });
277
+
278
+ it("ignores excluded fields on read", async () => {
279
+ const wrapper = new ExcelWrapper(fullSchema);
280
+
281
+ // Create Excel with all fields
282
+ const records = [{ name: "John Doe", age: 30, email: "john@test.com", phone: "010-1234-5678" }];
283
+ const wb = await wrapper.write("Test", records);
284
+ const buffer = await wb.getBytes();
285
+ await wb.close();
286
+
287
+ // Read with excludes
288
+ const readRecords = await wrapper.read(buffer, "Test", { excludes: ["email", "phone"] });
289
+
290
+ expect(readRecords[0].name).toBe("John Doe");
291
+ expect(readRecords[0].age).toBe(30);
292
+ expect(readRecords[0].email).toBeUndefined();
293
+ expect(readRecords[0].phone).toBeUndefined();
294
+ });
295
+ });
296
+ });
Binary file
@@ -0,0 +1,190 @@
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.createWorksheet("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.getBytes();
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.createWorksheet("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.getBytes();
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.createWorksheet("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
+ });