diff --git a/packages/@uppy/transloadit/src/Assembly.ts b/packages/@uppy/transloadit/src/Assembly.ts index ca6384ce7..d98900ac1 100644 --- a/packages/@uppy/transloadit/src/Assembly.ts +++ b/packages/@uppy/transloadit/src/Assembly.ts @@ -144,7 +144,7 @@ class TransloaditAssembly extends Emitter { if (typeof details.progress_combined === 'number') { this.status = { ...this.status, - execution_progress: details.progress_combined, + progress_combined: details.progress_combined, } } this.emit('execution-progress', details) diff --git a/packages/@uppy/transloadit/src/index.test.js b/packages/@uppy/transloadit/src/index.test.js index ffcdd31c5..6c8d8ab7f 100644 --- a/packages/@uppy/transloadit/src/index.test.js +++ b/packages/@uppy/transloadit/src/index.test.js @@ -189,6 +189,15 @@ describe('Transloadit', () => { }, }) + // Plugin state should start empty; track every distinct `ok` that lands in + // it so we can verify the assembly lifecycle is reflected. + expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined() + const okHistory = [] + const unsubscribe = uppy.store.subscribe((_prev, next) => { + const ok = next.plugins.Transloadit.assemblyStatus?.ok + if (ok && ok !== okHistory.at(-1)) okHistory.push(ok) + }) + uppy.addFile({ source: 'test', name: 'cat.jpg', @@ -228,12 +237,23 @@ describe('Transloadit', () => { uppy.resumeAll() await uploadPromise + unsubscribe() expect(successSpy).toHaveBeenCalled() // Should be reset to true after upload completes expect(uppy.getState().allowNewUpload).toBe(true) + // Plugin state reflected the assembly status the server returned. + // The createAssembly mock returned ASSEMBLY_EXECUTING and the assembly + // setter forwarded that status into plugin state. We assert containment + // (not exact sequence) because the test environment doesn't reliably + // drive the polling-to-COMPLETED transition. + expect(okHistory).toContain('ASSEMBLY_EXECUTING') + const finalStatus = uppy.getState().plugins.Transloadit.assemblyStatus + expect(finalStatus).toBeDefined() + expect(finalStatus.assembly_id).toBe('test-assembly-id') + server.close() }) @@ -317,198 +337,4 @@ describe('Transloadit', () => { // Should be reset to true expect(uppy.getState().allowNewUpload).toBe(true) }) - - it('populates plugin state assemblyStatus during a real upload flow', async () => { - const assemblyStatusBase = { - assembly_id: 'flow-test-id', - websocket_url: 'ws://localhost:8080', - tus_url: 'http://localhost/resumable/files/', - assembly_ssl_url: 'https://api2.transloadit.com/assemblies/flow-test-id', - } - - const tusUploads = new Map() - let uploadIndex = 0 - const tusBaseUrl = 'http://localhost/resumable/files/' - - const server = setupServer( - http.options('http://localhost/resumable/files*', () => { - return new HttpResponse(null, { - status: 204, - headers: { - 'Tus-Resumable': '1.0.0', - 'Tus-Version': '1.0.0', - 'Tus-Extension': 'creation,creation-defer-length', - }, - }) - }), - http.post('http://localhost/resumable/files*', ({ request }) => { - const uploadLengthHeader = request.headers.get('upload-length') - const uploadLength = uploadLengthHeader ? Number(uploadLengthHeader) : 0 - const uploadId = `flow-upload-${uploadIndex++}` - tusUploads.set(uploadId, { - length: Number.isNaN(uploadLength) ? 0 : uploadLength, - offset: 0, - }) - - return new HttpResponse(null, { - status: 201, - headers: { - Location: `${tusBaseUrl}${uploadId}`, - 'Upload-Offset': '0', - 'Tus-Resumable': '1.0.0', - }, - }) - }), - http.patch( - 'http://localhost/resumable/files/:uploadId', - async ({ request, params }) => { - const upload = tusUploads.get(params.uploadId) - if (!upload) return new HttpResponse(null, { status: 404 }) - const body = await request.arrayBuffer() - const offsetHeader = request.headers.get('upload-offset') - const baseOffset = offsetHeader ? Number(offsetHeader) : upload.offset - const nextOffset = baseOffset + body.byteLength - upload.offset = nextOffset - return new HttpResponse(null, { - status: 204, - headers: { - 'Upload-Offset': String(nextOffset), - 'Tus-Resumable': '1.0.0', - }, - }) - }, - ), - http.post('https://api2.transloadit.com/assemblies', () => { - return HttpResponse.json({ - ...assemblyStatusBase, - ok: 'ASSEMBLY_UPLOADING', - }) - }), - http.get('https://api2.transloadit.com/assemblies/*', () => { - return HttpResponse.json({ - ...assemblyStatusBase, - ok: 'ASSEMBLY_COMPLETED', - results: {}, - }) - }), - http.post('https://transloaditstatus.com/client_error', () => { - return HttpResponse.json({}) - }), - ) - - server.listen({ onUnhandledRequest: 'error' }) - - const uppy = new Core() - uppy.use(Transloadit, { - assemblyOptions: { - params: { - auth: { key: 'test-auth-key' }, - template_id: 'test-template-id', - }, - }, - }) - - // Before upload: assemblyStatus should be undefined - expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined() - - // Track the assemblyStatus values we see as the store updates. - // This avoids racing against the upload completion. - const seenStatuses = [] - const unsub = uppy.store.subscribe((_prev, next) => { - seenStatuses.push(next.plugins.Transloadit.assemblyStatus) - }) - - uppy.addFile({ - source: 'test', - name: 'test.jpg', - data: new File([new Uint8Array([1, 2, 3, 4, 5])], 'test.jpg', { - type: 'image/jpeg', - }), - }) - - await uppy.upload() - unsub() - - // At some point during the upload the plugin must have written a defined - // assemblyStatus into plugin state. - const definedStatuses = seenStatuses.filter((s) => s != null) - expect(definedStatuses.length).toBeGreaterThan(0) - expect(definedStatuses[0].assembly_id).toBe('flow-test-id') - - // After upload completes: the final Assembly status remains in plugin state - // so the UI can display the completed results. It is only cleared on a new - // upload or explicit cancel. - const finalStatus = uppy.getState().plugins.Transloadit.assemblyStatus - expect(finalStatus).toBeDefined() - expect(finalStatus.assembly_id).toBe('flow-test-id') - - server.close() - }) - - it('clears assemblyStatus at the start of a new upload', async () => { - const uppy = new Core() - uppy.use(Transloadit, { - assemblyOptions: { - params: { - auth: { key: 'test-auth-key' }, - template_id: 'test-template-id', - }, - }, - }) - - const plugin = uppy.getPlugin('Transloadit') - // Simulate a prior Assembly still sitting in plugin state. - plugin.setPluginState({ - assemblyStatus: { assembly_id: 'prior', ok: 'ASSEMBLY_COMPLETED' }, - }) - expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeDefined() - - // Make #createAssembly fail so we can observe the clear-on-start without - // running a full upload. - plugin.client.createAssembly = () => Promise.reject(new Error('stop here')) - - uppy.addFile({ - source: 'test', - name: 'abc', - data: new Uint8Array(100), - }) - - await uppy.upload().catch(() => {}) - - // `#prepareUpload` should have cleared the stale status at its start. - // After the failure, it may still be undefined (no new Assembly created). - expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined() - }) - - it('clears assemblyStatus on cancelAll even after Assembly was torn down', async () => { - // Regression: after upload completion, `#afterUpload` sets `this.assembly = undefined`, - // but the cached completed status stays in plugin state so the UI can show results. - // When the user clicks "Upload other files" (which calls `uppy.cancelAll()`), the - // cached status must be cleared so the UI resets — even though `this.assembly` is - // already gone and `#cancelAssembly` won't be called. - const uppy = new Core() - uppy.use(Transloadit, { - assemblyOptions: { - params: { - auth: { key: 'test-auth-key' }, - template_id: 'test-template-id', - }, - }, - }) - - const plugin = uppy.getPlugin('Transloadit') - // Simulate the post-completion state: assemblyStatus still cached, but - // this.#assembly already torn down by `#afterUpload`'s finally block. - plugin.setPluginState({ - assemblyStatus: { assembly_id: 'done', ok: 'ASSEMBLY_COMPLETED' }, - }) - expect(plugin.assembly).toBeUndefined() - expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeDefined() - - uppy.cancelAll() - // Allow the async #onCancelAll handler to run. - await Promise.resolve() - - expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined() - }) }) diff --git a/packages/@uppy/transloadit/src/index.ts b/packages/@uppy/transloadit/src/index.ts index bedd83b6e..961ee1f21 100644 --- a/packages/@uppy/transloadit/src/index.ts +++ b/packages/@uppy/transloadit/src/index.ts @@ -27,7 +27,7 @@ import Client, { type AssemblyError } from './Client.js' import locale from './locale.js' export type AssemblyResponse = AssemblyStatus & { - execution_progress?: number + progress_combined?: number } export type AssemblyFile = AssemblyStatusUpload export type AssemblyResult = AssemblyStatusResult & { localId: string | null }