diff --git a/extensions/clickclack/src/http-client.test.ts b/extensions/clickclack/src/http-client.test.ts index 37002a3ebc7f..07339cf076c2 100644 --- a/extensions/clickclack/src/http-client.test.ts +++ b/extensions/clickclack/src/http-client.test.ts @@ -641,6 +641,14 @@ describe("ClickClack HTTP client", () => { }); it("uploads multipart bytes with filename and MIME, then attaches by id", async () => { + const NativeBlob = Blob; + let uploadBlobPart: BlobPart | undefined; + class CapturingBlob extends NativeBlob { + constructor(parts?: BlobPart[], options?: BlobPropertyBag) { + uploadBlobPart = parts?.[0]; + super(parts, options); + } + } const fetchMock = vi .fn() .mockResolvedValueOnce( @@ -669,15 +677,24 @@ describe("ClickClack HTTP client", () => { fetch: fetchMock as unknown as typeof fetch, }); - const upload = await client.createUpload({ - workspaceId: "wsp_1", - buffer: Buffer.from("const proof = true;"), - filename: "viewer-proof.ts", - contentType: "text/typescript", - nonce: "upload-queue-1", - }); + const uploadBuffer = Buffer.from("const proof = true;"); + vi.stubGlobal("Blob", CapturingBlob); + const upload = await client + .createUpload({ + workspaceId: "wsp_1", + buffer: uploadBuffer, + filename: "viewer-proof.ts", + contentType: "text/typescript", + nonce: "upload-queue-1", + }) + .finally(() => vi.unstubAllGlobals()); await client.attachUpload("msg_1", upload.id); + expect(uploadBlobPart).toBeInstanceOf(Uint8Array); + const uploadBytes = uploadBlobPart as Uint8Array; + expect(uploadBytes.buffer).toBe(uploadBuffer.buffer); + expect(uploadBytes.byteOffset).toBe(uploadBuffer.byteOffset); + expect(uploadBytes.byteLength).toBe(uploadBuffer.byteLength); const uploadRequest = fetchMock.mock.calls[0]; expect(uploadRequest?.[0]).toBe( "https://clickclack.example/api/uploads?workspace_id=wsp_1&nonce=upload-queue-1", diff --git a/extensions/clickclack/src/http-client.ts b/extensions/clickclack/src/http-client.ts index d90b02aec714..21268a8ae93b 100644 --- a/extensions/clickclack/src/http-client.ts +++ b/extensions/clickclack/src/http-client.ts @@ -474,7 +474,10 @@ export function createClickClackClient(options: ClientOptions) { nonce?: string; }): Promise => { const form = new FormData(); - const bytes = new Uint8Array(params.buffer); + const bytes: Uint8Array = + params.buffer.buffer instanceof ArrayBuffer + ? new Uint8Array(params.buffer.buffer, params.buffer.byteOffset, params.buffer.byteLength) + : Uint8Array.from(params.buffer); form.append("file", new Blob([bytes], { type: params.contentType }), params.filename); const query = new URLSearchParams({ workspace_id: params.workspaceId }); if (params.nonce) {