@openclaw/bluebubbles 2026.1.29
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/index.ts +20 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +33 -0
- package/src/accounts.ts +80 -0
- package/src/actions.test.ts +651 -0
- package/src/actions.ts +403 -0
- package/src/attachments.test.ts +346 -0
- package/src/attachments.ts +282 -0
- package/src/channel.ts +399 -0
- package/src/chat.test.ts +462 -0
- package/src/chat.ts +354 -0
- package/src/config-schema.ts +51 -0
- package/src/media-send.ts +168 -0
- package/src/monitor.test.ts +2146 -0
- package/src/monitor.ts +2276 -0
- package/src/onboarding.ts +340 -0
- package/src/probe.ts +127 -0
- package/src/reactions.test.ts +393 -0
- package/src/reactions.ts +183 -0
- package/src/runtime.ts +14 -0
- package/src/send.test.ts +809 -0
- package/src/send.ts +418 -0
- package/src/targets.test.ts +184 -0
- package/src/targets.ts +323 -0
- package/src/types.ts +127 -0
package/src/chat.test.ts
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { markBlueBubblesChatRead, sendBlueBubblesTyping, setGroupIconBlueBubbles } from "./chat.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./accounts.js", () => ({
|
|
6
|
+
resolveBlueBubblesAccount: vi.fn(({ cfg, accountId }) => {
|
|
7
|
+
const config = cfg?.channels?.bluebubbles ?? {};
|
|
8
|
+
return {
|
|
9
|
+
accountId: accountId ?? "default",
|
|
10
|
+
enabled: config.enabled !== false,
|
|
11
|
+
configured: Boolean(config.serverUrl && config.password),
|
|
12
|
+
config,
|
|
13
|
+
};
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const mockFetch = vi.fn();
|
|
18
|
+
|
|
19
|
+
describe("chat", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
22
|
+
mockFetch.mockReset();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.unstubAllGlobals();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("markBlueBubblesChatRead", () => {
|
|
30
|
+
it("does nothing when chatGuid is empty", async () => {
|
|
31
|
+
await markBlueBubblesChatRead("", {
|
|
32
|
+
serverUrl: "http://localhost:1234",
|
|
33
|
+
password: "test",
|
|
34
|
+
});
|
|
35
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("does nothing when chatGuid is whitespace", async () => {
|
|
39
|
+
await markBlueBubblesChatRead(" ", {
|
|
40
|
+
serverUrl: "http://localhost:1234",
|
|
41
|
+
password: "test",
|
|
42
|
+
});
|
|
43
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("throws when serverUrl is missing", async () => {
|
|
47
|
+
await expect(markBlueBubblesChatRead("chat-guid", {})).rejects.toThrow(
|
|
48
|
+
"serverUrl is required",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("throws when password is missing", async () => {
|
|
53
|
+
await expect(
|
|
54
|
+
markBlueBubblesChatRead("chat-guid", {
|
|
55
|
+
serverUrl: "http://localhost:1234",
|
|
56
|
+
}),
|
|
57
|
+
).rejects.toThrow("password is required");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("marks chat as read successfully", async () => {
|
|
61
|
+
mockFetch.mockResolvedValueOnce({
|
|
62
|
+
ok: true,
|
|
63
|
+
text: () => Promise.resolve(""),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await markBlueBubblesChatRead("iMessage;-;+15551234567", {
|
|
67
|
+
serverUrl: "http://localhost:1234",
|
|
68
|
+
password: "test-password",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
72
|
+
expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/read"),
|
|
73
|
+
expect.objectContaining({ method: "POST" }),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("includes password in URL query", async () => {
|
|
78
|
+
mockFetch.mockResolvedValueOnce({
|
|
79
|
+
ok: true,
|
|
80
|
+
text: () => Promise.resolve(""),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await markBlueBubblesChatRead("chat-123", {
|
|
84
|
+
serverUrl: "http://localhost:1234",
|
|
85
|
+
password: "my-secret",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
89
|
+
expect(calledUrl).toContain("password=my-secret");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws on non-ok response", async () => {
|
|
93
|
+
mockFetch.mockResolvedValueOnce({
|
|
94
|
+
ok: false,
|
|
95
|
+
status: 404,
|
|
96
|
+
text: () => Promise.resolve("Chat not found"),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(
|
|
100
|
+
markBlueBubblesChatRead("missing-chat", {
|
|
101
|
+
serverUrl: "http://localhost:1234",
|
|
102
|
+
password: "test",
|
|
103
|
+
}),
|
|
104
|
+
).rejects.toThrow("read failed (404): Chat not found");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("trims chatGuid before using", async () => {
|
|
108
|
+
mockFetch.mockResolvedValueOnce({
|
|
109
|
+
ok: true,
|
|
110
|
+
text: () => Promise.resolve(""),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await markBlueBubblesChatRead(" chat-with-spaces ", {
|
|
114
|
+
serverUrl: "http://localhost:1234",
|
|
115
|
+
password: "test",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
119
|
+
expect(calledUrl).toContain("/api/v1/chat/chat-with-spaces/read");
|
|
120
|
+
expect(calledUrl).not.toContain("%20chat");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("resolves credentials from config", async () => {
|
|
124
|
+
mockFetch.mockResolvedValueOnce({
|
|
125
|
+
ok: true,
|
|
126
|
+
text: () => Promise.resolve(""),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await markBlueBubblesChatRead("chat-123", {
|
|
130
|
+
cfg: {
|
|
131
|
+
channels: {
|
|
132
|
+
bluebubbles: {
|
|
133
|
+
serverUrl: "http://config-server:9999",
|
|
134
|
+
password: "config-pass",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
141
|
+
expect(calledUrl).toContain("config-server:9999");
|
|
142
|
+
expect(calledUrl).toContain("password=config-pass");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("sendBlueBubblesTyping", () => {
|
|
147
|
+
it("does nothing when chatGuid is empty", async () => {
|
|
148
|
+
await sendBlueBubblesTyping("", true, {
|
|
149
|
+
serverUrl: "http://localhost:1234",
|
|
150
|
+
password: "test",
|
|
151
|
+
});
|
|
152
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("does nothing when chatGuid is whitespace", async () => {
|
|
156
|
+
await sendBlueBubblesTyping(" ", false, {
|
|
157
|
+
serverUrl: "http://localhost:1234",
|
|
158
|
+
password: "test",
|
|
159
|
+
});
|
|
160
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("throws when serverUrl is missing", async () => {
|
|
164
|
+
await expect(sendBlueBubblesTyping("chat-guid", true, {})).rejects.toThrow(
|
|
165
|
+
"serverUrl is required",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("throws when password is missing", async () => {
|
|
170
|
+
await expect(
|
|
171
|
+
sendBlueBubblesTyping("chat-guid", true, {
|
|
172
|
+
serverUrl: "http://localhost:1234",
|
|
173
|
+
}),
|
|
174
|
+
).rejects.toThrow("password is required");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("sends typing start with POST method", async () => {
|
|
178
|
+
mockFetch.mockResolvedValueOnce({
|
|
179
|
+
ok: true,
|
|
180
|
+
text: () => Promise.resolve(""),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await sendBlueBubblesTyping("iMessage;-;+15551234567", true, {
|
|
184
|
+
serverUrl: "http://localhost:1234",
|
|
185
|
+
password: "test",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
189
|
+
expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing"),
|
|
190
|
+
expect.objectContaining({ method: "POST" }),
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("sends typing stop with DELETE method", async () => {
|
|
195
|
+
mockFetch.mockResolvedValueOnce({
|
|
196
|
+
ok: true,
|
|
197
|
+
text: () => Promise.resolve(""),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await sendBlueBubblesTyping("iMessage;-;+15551234567", false, {
|
|
201
|
+
serverUrl: "http://localhost:1234",
|
|
202
|
+
password: "test",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
206
|
+
expect.stringContaining("/api/v1/chat/iMessage%3B-%3B%2B15551234567/typing"),
|
|
207
|
+
expect.objectContaining({ method: "DELETE" }),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("includes password in URL query", async () => {
|
|
212
|
+
mockFetch.mockResolvedValueOnce({
|
|
213
|
+
ok: true,
|
|
214
|
+
text: () => Promise.resolve(""),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await sendBlueBubblesTyping("chat-123", true, {
|
|
218
|
+
serverUrl: "http://localhost:1234",
|
|
219
|
+
password: "typing-secret",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
223
|
+
expect(calledUrl).toContain("password=typing-secret");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("throws on non-ok response", async () => {
|
|
227
|
+
mockFetch.mockResolvedValueOnce({
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 500,
|
|
230
|
+
text: () => Promise.resolve("Internal error"),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await expect(
|
|
234
|
+
sendBlueBubblesTyping("chat-123", true, {
|
|
235
|
+
serverUrl: "http://localhost:1234",
|
|
236
|
+
password: "test",
|
|
237
|
+
}),
|
|
238
|
+
).rejects.toThrow("typing failed (500): Internal error");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("trims chatGuid before using", async () => {
|
|
242
|
+
mockFetch.mockResolvedValueOnce({
|
|
243
|
+
ok: true,
|
|
244
|
+
text: () => Promise.resolve(""),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await sendBlueBubblesTyping(" trimmed-chat ", true, {
|
|
248
|
+
serverUrl: "http://localhost:1234",
|
|
249
|
+
password: "test",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
253
|
+
expect(calledUrl).toContain("/api/v1/chat/trimmed-chat/typing");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("encodes special characters in chatGuid", async () => {
|
|
257
|
+
mockFetch.mockResolvedValueOnce({
|
|
258
|
+
ok: true,
|
|
259
|
+
text: () => Promise.resolve(""),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await sendBlueBubblesTyping("iMessage;+;group@chat.com", true, {
|
|
263
|
+
serverUrl: "http://localhost:1234",
|
|
264
|
+
password: "test",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
268
|
+
expect(calledUrl).toContain("iMessage%3B%2B%3Bgroup%40chat.com");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("resolves credentials from config", async () => {
|
|
272
|
+
mockFetch.mockResolvedValueOnce({
|
|
273
|
+
ok: true,
|
|
274
|
+
text: () => Promise.resolve(""),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await sendBlueBubblesTyping("chat-123", true, {
|
|
278
|
+
cfg: {
|
|
279
|
+
channels: {
|
|
280
|
+
bluebubbles: {
|
|
281
|
+
serverUrl: "http://typing-server:8888",
|
|
282
|
+
password: "typing-pass",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
289
|
+
expect(calledUrl).toContain("typing-server:8888");
|
|
290
|
+
expect(calledUrl).toContain("password=typing-pass");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("can start and stop typing in sequence", async () => {
|
|
294
|
+
mockFetch
|
|
295
|
+
.mockResolvedValueOnce({
|
|
296
|
+
ok: true,
|
|
297
|
+
text: () => Promise.resolve(""),
|
|
298
|
+
})
|
|
299
|
+
.mockResolvedValueOnce({
|
|
300
|
+
ok: true,
|
|
301
|
+
text: () => Promise.resolve(""),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await sendBlueBubblesTyping("chat-123", true, {
|
|
305
|
+
serverUrl: "http://localhost:1234",
|
|
306
|
+
password: "test",
|
|
307
|
+
});
|
|
308
|
+
await sendBlueBubblesTyping("chat-123", false, {
|
|
309
|
+
serverUrl: "http://localhost:1234",
|
|
310
|
+
password: "test",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
314
|
+
expect(mockFetch.mock.calls[0][1].method).toBe("POST");
|
|
315
|
+
expect(mockFetch.mock.calls[1][1].method).toBe("DELETE");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("setGroupIconBlueBubbles", () => {
|
|
320
|
+
it("throws when chatGuid is empty", async () => {
|
|
321
|
+
await expect(
|
|
322
|
+
setGroupIconBlueBubbles("", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
323
|
+
serverUrl: "http://localhost:1234",
|
|
324
|
+
password: "test",
|
|
325
|
+
}),
|
|
326
|
+
).rejects.toThrow("chatGuid");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("throws when buffer is empty", async () => {
|
|
330
|
+
await expect(
|
|
331
|
+
setGroupIconBlueBubbles("chat-guid", new Uint8Array(0), "icon.png", {
|
|
332
|
+
serverUrl: "http://localhost:1234",
|
|
333
|
+
password: "test",
|
|
334
|
+
}),
|
|
335
|
+
).rejects.toThrow("image buffer");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("throws when serverUrl is missing", async () => {
|
|
339
|
+
await expect(
|
|
340
|
+
setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {}),
|
|
341
|
+
).rejects.toThrow("serverUrl is required");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("throws when password is missing", async () => {
|
|
345
|
+
await expect(
|
|
346
|
+
setGroupIconBlueBubbles("chat-guid", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
347
|
+
serverUrl: "http://localhost:1234",
|
|
348
|
+
}),
|
|
349
|
+
).rejects.toThrow("password is required");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("sets group icon successfully", async () => {
|
|
353
|
+
mockFetch.mockResolvedValueOnce({
|
|
354
|
+
ok: true,
|
|
355
|
+
text: () => Promise.resolve(""),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const buffer = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG magic bytes
|
|
359
|
+
await setGroupIconBlueBubbles("iMessage;-;chat-guid", buffer, "icon.png", {
|
|
360
|
+
serverUrl: "http://localhost:1234",
|
|
361
|
+
password: "test-password",
|
|
362
|
+
contentType: "image/png",
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
366
|
+
expect.stringContaining("/api/v1/chat/iMessage%3B-%3Bchat-guid/icon"),
|
|
367
|
+
expect.objectContaining({
|
|
368
|
+
method: "POST",
|
|
369
|
+
headers: expect.objectContaining({
|
|
370
|
+
"Content-Type": expect.stringContaining("multipart/form-data"),
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("includes password in URL query", async () => {
|
|
377
|
+
mockFetch.mockResolvedValueOnce({
|
|
378
|
+
ok: true,
|
|
379
|
+
text: () => Promise.resolve(""),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
383
|
+
serverUrl: "http://localhost:1234",
|
|
384
|
+
password: "my-secret",
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
388
|
+
expect(calledUrl).toContain("password=my-secret");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("throws on non-ok response", async () => {
|
|
392
|
+
mockFetch.mockResolvedValueOnce({
|
|
393
|
+
ok: false,
|
|
394
|
+
status: 500,
|
|
395
|
+
text: () => Promise.resolve("Internal error"),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await expect(
|
|
399
|
+
setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "icon.png", {
|
|
400
|
+
serverUrl: "http://localhost:1234",
|
|
401
|
+
password: "test",
|
|
402
|
+
}),
|
|
403
|
+
).rejects.toThrow("setGroupIcon failed (500): Internal error");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it("trims chatGuid before using", async () => {
|
|
407
|
+
mockFetch.mockResolvedValueOnce({
|
|
408
|
+
ok: true,
|
|
409
|
+
text: () => Promise.resolve(""),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await setGroupIconBlueBubbles(" chat-with-spaces ", new Uint8Array([1]), "icon.png", {
|
|
413
|
+
serverUrl: "http://localhost:1234",
|
|
414
|
+
password: "test",
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
418
|
+
expect(calledUrl).toContain("/api/v1/chat/chat-with-spaces/icon");
|
|
419
|
+
expect(calledUrl).not.toContain("%20chat");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("resolves credentials from config", async () => {
|
|
423
|
+
mockFetch.mockResolvedValueOnce({
|
|
424
|
+
ok: true,
|
|
425
|
+
text: () => Promise.resolve(""),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1]), "icon.png", {
|
|
429
|
+
cfg: {
|
|
430
|
+
channels: {
|
|
431
|
+
bluebubbles: {
|
|
432
|
+
serverUrl: "http://config-server:9999",
|
|
433
|
+
password: "config-pass",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const calledUrl = mockFetch.mock.calls[0][0] as string;
|
|
440
|
+
expect(calledUrl).toContain("config-server:9999");
|
|
441
|
+
expect(calledUrl).toContain("password=config-pass");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("includes filename in multipart body", async () => {
|
|
445
|
+
mockFetch.mockResolvedValueOnce({
|
|
446
|
+
ok: true,
|
|
447
|
+
text: () => Promise.resolve(""),
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await setGroupIconBlueBubbles("chat-123", new Uint8Array([1, 2, 3]), "custom-icon.jpg", {
|
|
451
|
+
serverUrl: "http://localhost:1234",
|
|
452
|
+
password: "test",
|
|
453
|
+
contentType: "image/jpeg",
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const body = mockFetch.mock.calls[0][1].body as Uint8Array;
|
|
457
|
+
const bodyString = new TextDecoder().decode(body);
|
|
458
|
+
expect(bodyString).toContain('filename="custom-icon.jpg"');
|
|
459
|
+
expect(bodyString).toContain("image/jpeg");
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
});
|