diff --git a/.changeset/spotty-spoons-chew.md b/.changeset/spotty-spoons-chew.md new file mode 100644 index 0000000000..5cfcf204b2 --- /dev/null +++ b/.changeset/spotty-spoons-chew.md @@ -0,0 +1,5 @@ +--- +"uploadthing": patch +--- + +fix: run `onUploadBegin` diff --git a/docs/src/app/(docs)/api-reference/client/page.mdx b/docs/src/app/(docs)/api-reference/client/page.mdx index 0a05ccee86..8b42ae83e9 100644 --- a/docs/src/app/(docs)/api-reference/client/page.mdx +++ b/docs/src/app/(docs)/api-reference/client/page.mdx @@ -55,9 +55,9 @@ is an options object: An abort signal to abort the upload. - + Callback function called after the presigned URLs have been retrieved, just before - the files are uploaded to the storage provider. + the file is uploaded. Called once per file. ( returned are the files that will be uploaded, meaning you can use this to e.g. rename or resize the files. - - Callback function called after the presigned URLs have been retrieved, just - before the files are uploaded to the storage provider. + + Callback function called after the presigned URLs have been retrieved, just before + the file is uploaded. Called once per file. Disables the button. @@ -405,9 +405,9 @@ export const OurUploadDropzone = () => ( returned are the files that will be uploaded, meaning you can use this to e.g. rename or resize the files. - - Callback function called after the presigned URLs have been retrieved, just - before the files are uploaded to the storage provider. + + Callback function called after the presigned URLs have been retrieved, just before + the file is uploaded. Called once per file. Disables the button. @@ -512,7 +512,7 @@ is an options object: Callback function that gets continuously called as the file is uploaded to the storage provider. - + Callback function called after the presigned URLs have been retrieved, just before the files are uploaded to the storage provider. @@ -565,8 +565,8 @@ export function MultiUploader() { onUploadError: () => { alert("error occurred while uploading"); }, - onUploadBegin: () => { - alert("upload has begun"); + onUploadBegin: ({ file }) => { + console.log("upload has begun for", file); }, }); diff --git a/packages/react/test/upload-button.test.tsx b/packages/react/test/upload-button.test.tsx index 36c4630211..db05b92ba7 100644 --- a/packages/react/test/upload-button.test.tsx +++ b/packages/react/test/upload-button.test.tsx @@ -31,7 +31,13 @@ const testRouter = { pdf: f({ "application/pdf": {} }).onUploadComplete(noop), multi: f({ image: { maxFileCount: 4 } }).onUploadComplete(noop), }; -const routeHandler = createRouteHandler({ router: testRouter }); +const routeHandler = createRouteHandler({ + router: testRouter, + config: { + token: + "eyJhcHBJZCI6ImFwcC0xIiwiYXBpS2V5Ijoic2tfZm9vIiwicmVnaW9ucyI6WyJmcmExIl19", + }, +}); const UploadButton = generateUploadButton(); const utGet = vi.fn<(req: Request) => void>(); @@ -44,13 +50,16 @@ const server = setupServer( return routeHandler(request); }), http.post("/api/uploadthing", async ({ request }) => { - const body = await request.json(); + const body = await request.clone().json(); utPost({ request, body }); - return HttpResponse.json([ - // empty array, we're not testing the upload endpoint here - // we have other tests for that... - ]); + return routeHandler(request); }), + http.all<{ key: string }>( + "https://fra1.ingest.uploadthing.com/:key", + ({ request, params }) => { + return HttpResponse.json({ url: "https://utfs.io/f/" + params.key }); + }, + ), ); beforeAll(() => server.listen()); @@ -203,6 +212,25 @@ describe("UploadButton - lifecycle hooks", () => { ); }); }); + + it("onUploadBegin runs before uploading", async () => { + const onUploadBegin = vi.fn(); + const utils = render( + , + ); + await waitFor(() => { + expect(utils.getByText("Choose File(s)")).toBeInTheDocument(); + }); + + fireEvent.change(utils.getByLabelText("Choose File(s)"), { + target: { files: [new File([""], "foo.png"), new File([""], "bar.png")] }, + }); + await waitFor(() => { + expect(onUploadBegin).toHaveBeenCalledTimes(2); + }); + expect(onUploadBegin).toHaveBeenCalledWith("foo.png"); + expect(onUploadBegin).toHaveBeenCalledWith("bar.png"); + }); }); describe("UploadButton - Theming", () => { diff --git a/packages/uploadthing/src/internal/upload.browser.ts b/packages/uploadthing/src/internal/upload.browser.ts index 1b20257637..bb999a8339 100644 --- a/packages/uploadthing/src/internal/upload.browser.ts +++ b/packages/uploadthing/src/internal/upload.browser.ts @@ -146,22 +146,32 @@ export const uploadFilesInternal = < Micro.forEach( presigneds, (presigned, i) => - uploadFile( - opts.files[i], - presigned, - { - onUploadProgress: (ev) => { - totalLoaded += ev.delta; - opts.onUploadProgress?.({ - file: opts.files[i], - progress: Math.round((ev.loaded / opts.files[i].size) * 100), - loaded: ev.loaded, - delta: ev.delta, - totalLoaded, - totalProgress: Math.round((totalLoaded / totalSize) * 100), - }); - }, - }, + Micro.flatMap( + Micro.sync(() => + opts.onUploadBegin?.({ file: opts.files[i].name }), + ), + () => + uploadFile( + opts.files[i], + presigned, + { + onUploadProgress: (ev) => { + totalLoaded += ev.delta; + opts.onUploadProgress?.({ + file: opts.files[i], + progress: Math.round( + (ev.loaded / opts.files[i].size) * 100, + ), + loaded: ev.loaded, + delta: ev.delta, + totalLoaded, + totalProgress: Math.round( + (totalLoaded / totalSize) * 100, + ), + }); + }, + }, + ), ), { concurrency: 6 }, ), diff --git a/packages/uploadthing/test/client.test.ts b/packages/uploadthing/test/client.test.ts index 751509f891..2d72bc8fb8 100644 --- a/packages/uploadthing/test/client.test.ts +++ b/packages/uploadthing/test/client.test.ts @@ -2,7 +2,7 @@ import type { AddressInfo } from "node:net"; import express from "express"; -import { describe, expect, expectTypeOf, it as rawIt } from "vitest"; +import { describe, expect, expectTypeOf, it as rawIt, vi } from "vitest"; import { genUploader } from "../src/client"; import { createRouteHandler, createUploadthing } from "../src/express"; @@ -34,6 +34,13 @@ export const setupUTServer = async () => { }) .onUploadError(onErrorMock) .onUploadComplete(uploadCompleteMock), + multi: f({ text: { maxFileSize: "16MB", maxFileCount: 2 } }) + .middleware((opts) => { + middlewareMock(opts); + return {}; + }) + .onUploadError(onErrorMock) + .onUploadComplete(uploadCompleteMock), withServerData: f( { text: { maxFileSize: "4MB" } }, { awaitServerData: true }, @@ -264,6 +271,21 @@ describe("uploadFiles", () => { await close(); }); + it("handles too many files errors", async ({ db }) => { + const { uploadFiles, close } = await setupUTServer(); + + const file1 = new File(["foo"], "foo.txt", { type: "text/plain" }); + const file2 = new File(["bar"], "bar.txt", { type: "text/plain" }); + + await expect( + uploadFiles("foo", { files: [file1, file2] }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[UploadThingError: Invalid config: FileCountMismatch]`, + ); + + await close(); + }); + it("handles invalid file type errors", async ({ db }) => { const { uploadFiles, close } = await setupUTServer(); @@ -277,4 +299,38 @@ describe("uploadFiles", () => { await close(); }); + + it("runs onUploadBegin before uploading (single file)", async () => { + const { uploadFiles, close } = await setupUTServer(); + + const file = new File(["foo"], "foo.txt", { type: "text/plain" }); + const onUploadBegin = vi.fn(); + + await uploadFiles("foo", { + files: [file], + onUploadBegin, + }); + + expect(onUploadBegin).toHaveBeenCalledWith({ file: "foo.txt" }); + + await close(); + }); + + it("runs onUploadBegin before uploading (multi file)", async () => { + const { uploadFiles, close } = await setupUTServer(); + + const file1 = new File(["foo"], "foo.txt", { type: "text/plain" }); + const file2 = new File(["bar"], "bar.txt", { type: "text/plain" }); + const onUploadBegin = vi.fn(); + + await uploadFiles("multi", { + files: [file1, file2], + onUploadBegin, + }); + + expect(onUploadBegin).toHaveBeenCalledWith({ file: "foo.txt" }); + expect(onUploadBegin).toHaveBeenCalledWith({ file: "bar.txt" }); + + await close(); + }); });