diff --git a/packages/upload/src/vaadin-upload-mixin.js b/packages/upload/src/vaadin-upload-mixin.js index b64f1e8f96..e5305c3607 100644 --- a/packages/upload/src/vaadin-upload-mixin.js +++ b/packages/upload/src/vaadin-upload-mixin.js @@ -695,7 +695,24 @@ export const UploadMixin = (superClass) => files = [files]; } files = files.filter((file) => !file.complete); - Array.prototype.forEach.call(files, this._uploadFile.bind(this)); + // Upload only the first file in the queue, not all at once + if (files.length > 0) { + this._uploadFile(files[0]); + } + } + + /** @private */ + _processNextFileInQueue() { + // Find the next file that is queued but not yet uploaded + // Search from the end since files are prepended (newest first) + // This ensures files upload in the order they were added + const nextFile = this.files + .slice() + .reverse() + .find((file) => !file.complete && !file.uploading && !file.abort); + if (nextFile) { + this._uploadFile(nextFile); + } } /** @private */ @@ -776,6 +793,8 @@ export const UploadMixin = (superClass) => }), ); this._renderFileList(); + // Process the next file in the queue after this one completes + this._processNextFileInQueue(); } }; @@ -881,6 +900,8 @@ export const UploadMixin = (superClass) => file.xhr.abort(); } this._removeFile(file); + // Process the next file in the queue after aborting this one + this._processNextFileInQueue(); } } @@ -934,7 +955,11 @@ export const UploadMixin = (superClass) => this.files = [file, ...this.files]; if (!this.noAuto) { - this._uploadFile(file); + // Only start uploading if no other file is currently being uploaded + const isAnyFileUploading = this.files.some((f) => f.uploading); + if (!isAnyFileUploading) { + this._uploadFile(file); + } } } diff --git a/packages/upload/test/adding-files.test.js b/packages/upload/test/adding-files.test.js index 8e2787e142..85dc36c744 100644 --- a/packages/upload/test/adding-files.test.js +++ b/packages/upload/test/adding-files.test.js @@ -336,8 +336,12 @@ describe('adding files', () => { upload.addEventListener('upload-start', uploadStartSpy); files.forEach(upload._addFile.bind(upload)); - expect(uploadStartSpy.calledTwice).to.be.true; - expect(upload.files[0].held).to.be.false; + // With queue behavior, only the first file starts uploading immediately + expect(uploadStartSpy.calledOnce).to.be.true; + // Files are prepended, so the first file added is at index 1 + expect(upload.files[1].held).to.be.false; + // Second file (at index 0) should be held in queue + expect(upload.files[0].held).to.be.true; }); it('should not automatically start upload when noAuto flag is set', () => { diff --git a/packages/upload/test/upload.test.js b/packages/upload/test/upload.test.js index ecf5664969..0fd5299277 100644 --- a/packages/upload/test/upload.test.js +++ b/packages/upload/test/upload.test.js @@ -437,16 +437,21 @@ describe('upload', () => { upload.files.forEach((file) => { expect(file.uploading).not.to.be.ok; }); + let firstUploadStartFired = false; upload.addEventListener('upload-start', (e) => { - expect(e.detail.xhr).to.be.ok; - expect(e.detail.file).to.be.ok; - expect(e.detail.file.name).to.equal(tempFileName); - expect(e.detail.file.uploading).to.be.ok; + if (!firstUploadStartFired) { + firstUploadStartFired = true; + expect(e.detail.xhr).to.be.ok; + expect(e.detail.file).to.be.ok; + expect(e.detail.file.name).to.equal(tempFileName); + expect(e.detail.file.uploading).to.be.ok; - for (let i = 0; i < upload.files.length - 1; i++) { - expect(upload.files[i].uploading).not.to.be.ok; + for (let i = 0; i < upload.files.length - 1; i++) { + expect(upload.files[i].uploading).not.to.be.ok; + } + done(); } - done(); + // With queue behavior, other files will start after the first completes - ignore those events }); upload.uploadFiles([upload.files[2]]); }); @@ -539,6 +544,141 @@ describe('upload', () => { }); }); + describe('Upload Queue', () => { + let clock, files; + + beforeEach(() => { + upload._createXhr = xhrCreator({ size: file.size, uploadTime: 200, stepTime: 50 }); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should upload multiple files one at a time', async () => { + files = createFiles(3, 512, 'application/json'); + upload._addFiles(files); + + // Files are prepended, so files[0] is at index 2, files[1] at index 1, files[2] at index 0 + // First file added (files[0]) should start uploading + await clock.tickAsync(10); + expect(upload.files[2].uploading).to.be.true; + expect(upload.files[2].held).to.be.false; + expect(upload.files[1].held).to.be.true; + expect(upload.files[0].held).to.be.true; + + // Wait for first file to complete (connectTime + uploadTime + serverTime = 10 + 200 + 10 = 220ms) + await clock.tickAsync(220); + expect(upload.files[2].complete).to.be.true; + expect(upload.files[2].uploading).to.be.false; + + // Second file (files[1]) should now start uploading + await clock.tickAsync(10); + expect(upload.files[1].uploading).to.be.true; + expect(upload.files[1].held).to.be.false; + expect(upload.files[0].held).to.be.true; + + // Wait for second file to complete + await clock.tickAsync(220); + expect(upload.files[1].complete).to.be.true; + expect(upload.files[1].uploading).to.be.false; + + // Third file (files[2]) should now start uploading + await clock.tickAsync(10); + expect(upload.files[0].uploading).to.be.true; + expect(upload.files[0].held).to.be.false; + + // Wait for third file to complete + await clock.tickAsync(220); + expect(upload.files[0].complete).to.be.true; + expect(upload.files[0].uploading).to.be.false; + }); + + it('should process next file in queue after one completes with error', async () => { + upload._createXhr = xhrCreator({ + size: 512, + uploadTime: 200, + stepTime: 50, + serverValidation: () => { + return { status: 500, statusText: 'Server Error' }; + }, + }); + + const errorSpy = sinon.spy(); + const startSpy = sinon.spy(); + upload.addEventListener('upload-error', errorSpy); + upload.addEventListener('upload-start', startSpy); + + files = createFiles(2, 512, 'application/json'); + upload._addFiles(files); + + // First file should start + await clock.tickAsync(10); + expect(startSpy.callCount).to.equal(1); + + // Wait for first file to complete with error + await clock.tickAsync(220); + expect(errorSpy.callCount).to.equal(1); + + // Second file should now start + await clock.tickAsync(10); + expect(startSpy.callCount).to.equal(2); + expect(upload.files.some((f) => f.uploading)).to.be.true; + }); + + it('should process next file in queue after one is aborted', async () => { + files = createFiles(2, 512, 'application/json'); + upload._addFiles(files); + + // First file added (at index 1) should start uploading + await clock.tickAsync(10); + expect(upload.files[1].uploading).to.be.true; + expect(upload.files[0].held).to.be.true; + + // Abort the first file (at index 1) + upload._abortFileUpload(upload.files[1]); + + // Second file (now at index 0 after first is removed) should now start uploading + await clock.tickAsync(10); + expect(upload.files[0].uploading).to.be.true; + }); + + it('should only start one file when uploadFiles is called with multiple files', async () => { + upload.noAuto = true; + files = createFiles(3, 512, 'application/json'); + upload._addFiles(files); + + // No files should be uploading yet - all should be held + await clock.tickAsync(10); + expect(upload.files[0].held).to.be.true; + expect(upload.files[1].held).to.be.true; + expect(upload.files[2].held).to.be.true; + + // Call uploadFiles + upload.uploadFiles(); + + // Only first file (at index 2) should start uploading - wait for it to begin + await clock.tickAsync(20); + expect(upload.files.length).to.equal(3); + // One file should be uploading (the oldest one added) + const uploadingFile = upload.files.find((f) => f.uploading); + expect(uploadingFile).to.be.ok; + // The other two should still be held + const heldFiles = upload.files.filter((f) => f.held); + expect(heldFiles.length).to.equal(2); + + // Wait for first file to complete + await clock.tickAsync(220); + + // Second file should start automatically + await clock.tickAsync(10); + expect(upload.files.some((f) => f.uploading)).to.be.true; + const remainingHeldFiles = upload.files.filter((f) => f.held); + expect(remainingHeldFiles.length).to.equal(1); + }); + }); + describe('Upload format', () => { let clock;