Skip to content

Commit

Permalink
Merge pull request #1530 from nextcloud-libraries/feat/assembly
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv authored Feb 25, 2025
2 parents d4c7eb8 + 5d01576 commit 2f8ce18
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 58 deletions.
6 changes: 1 addition & 5 deletions cypress/components/UploadPicker/UploadPicker.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ describe('UploadPicker valid uploads', () => {
afterEach(() => resetDocument())

it('Uploads a file with chunking', () => {
// Init and reset chunk request spy
const chunksRequestsSpy = cy.spy()

// Intercept tmp upload chunks folder creation
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
statusCode: 201,
Expand All @@ -151,7 +148,6 @@ describe('UploadPicker valid uploads', () => {
method: 'PUT',
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
}, (req) => {
chunksRequestsSpy()
req.reply({
statusCode: 201,
})
Expand Down Expand Up @@ -193,7 +189,7 @@ describe('UploadPicker valid uploads', () => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('not.be.visible')
expect(chunksRequestsSpy).to.have.always.been.callCount(26)
cy.get('@chunks.all').should('have.lengthOf', 26)
})
})

Expand Down
74 changes: 72 additions & 2 deletions cypress/components/UploadPicker/progress.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ describe('UploadPicker: progress handling', () => {
}),
}

// Start paused
getUploader(false, true).pause()

// Mount picker
cy.mount(UploadPicker, {
propsData,
Expand All @@ -74,7 +77,6 @@ describe('UploadPicker: progress handling', () => {
it('has increasing progress bar during non-chunked upload', () => {
// Start in paused mode
const uploader = getUploader()
uploader.pause()

cy.get('@input').attachFile({
// file of 5 MiB
Expand Down Expand Up @@ -150,7 +152,6 @@ describe('UploadPicker: progress handling', () => {

// Start in paused mode
const uploader = getUploader()
uploader.pause()

// 3 MiB/s meaning upload will take 5 seconds
throttleUpload(3 * 1024 * 1024)
Expand Down Expand Up @@ -205,6 +206,74 @@ describe('UploadPicker: progress handling', () => {
cy.get('@progress')
.should('not.be.visible')
})

it('shows the progress bar while assembling', () => {
// Maximum the responses can take
Cypress.config({ defaultCommandTimeout: 7000 })

const { promise, resolve } = Promise.withResolvers<void>()

cy.intercept('PUT', '/remote.php/dav/files/user/file.txt', { statusCode: 201 }).as('upload')
cy.intercept('MKCOL', '/remote.php/dav/uploads/user/*', { statusCode: 201 }).as('mkdir')
cy.intercept('PUT', '/remote.php/dav/uploads/user/*/*', (rq) => {
rq.reply({ statusCode: 201 })
if (rq.url.endsWith('/2')) {
rq.on('response', async () => await promise)
}
}).as('uploadBig')
cy.intercept('MOVE', '/remote.php/dav/uploads/user/*/.file', { statusCode: 201, delay: 1000 }).as('move')

// Start in paused mode
const uploader = getUploader()

cy.get('@input').attachFile([
{
// file of 5 MiB so it is not chunked
fileContent: new Blob([new ArrayBuffer(5 * 1024 * 1024)]),
fileName: 'file.txt',
mimeType: 'text/plain',
encoding: 'utf8',
lastModified: new Date().getTime(),
},
{
// file of 15 MiB so it is chunked in 10MiB and 5 MiB
fileContent: new Blob([new ArrayBuffer(15 * 1024 * 1024)]),
fileName: 'big-file.txt',
mimeType: 'text/plain',
encoding: 'utf8',
lastModified: new Date().getTime(),
},
])

// See there is no progress yet
cy.get('@progress')
.should('be.visible')
.should('have.value', 0)
cy.get('@progressLabel')
.should('contain.text', 'paused')
// start the uploader
.then(() => uploader.start())

// MKCOL was successfully so the upload can begin
cy.wait('@mkdir')

cy.get('@progress', { timeout: 2000 })
.should((el) => expect(el.val()).to.be.greaterThan(10))
.and((el) => expect(el.val()).to.be.lessThan(95))

cy.wait('@upload')
cy.wait('@uploadBig')
.then(() => resolve())

cy.get('@progressLabel')
.should('be.visible')
.and('contain.text', 'assembling')

cy.wait('@move')

cy.get('@progress')
.should('not.be.visible')
})
})

describe('UploadPicker: reset progress on retry', () => {
Expand All @@ -217,6 +286,7 @@ describe('UploadPicker: reset progress on retry', () => {
cy.window()
.then((win) => {
// Internal global variable
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(win as any)._oc_capabilities = { files: { chunked_upload: { max_parallel_count: 1 } } }
})

Expand Down
143 changes: 143 additions & 0 deletions cypress/components/UploadPicker/status.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint-disable no-unused-expressions */
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// dist file might not be built when running eslint only
// eslint-disable-next-line import/no-unresolved,n/no-missing-import
import { Folder, Permission } from '@nextcloud/files'
import { generateRemoteUrl } from '@nextcloud/router'
import { getUploader, UploadPicker } from '../../../lib/index.ts'

let state: string | undefined
before(() => {
cy.window().then((win) => {
state = win.document.body.innerHTML
})
})

const resetDocument = () => {
if (state) {
cy.window().then((win) => {
win.document.body.innerHTML = state!
})
}
}

describe('UploadPicker: status testing', () => {
beforeEach(() => {
// Make sure we reset the destination
// so other tests do not interfere
const propsData = {
destination: new Folder({
id: 56,
owner: 'user',
source: generateRemoteUrl('dav/files/user'),
permissions: Permission.ALL,
root: '/files/user',
}),
}

// Mount picker
const onPause = cy.spy().as('pausedListener')
const onResume = cy.spy().as('resumedListener')
cy.mount(UploadPicker, {
propsData,
listeners: {
paused: onPause,
resumed: onResume,
},
}).as('uploadPicker')

// Check and init aliases
cy.get('[data-cy-upload-picker] [data-cy-upload-picker-input]').as('input').should('exist')
cy.get('[data-cy-upload-picker] .upload-picker__progress').as('progress').should('exist')
})

afterEach(() => resetDocument())

it('shows paused status on pause', () => {
// Intercept tmp upload chunks folder creation
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
statusCode: 201,
}).as('init')

// Intercept chunks upload
cy.intercept({
method: 'PUT',
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
}, (req) => {
req.reply({
statusCode: 201,
})
}).as('chunks')

// Intercept final assembly request
const assemblyStartStub = cy.stub().as('assemblyStart')
cy.intercept('MOVE', '/remote.php/dav/uploads/*/web-file-upload*/.file', (req) => {
assemblyStartStub()
req.reply({
statusCode: 204,
// Fake assembling chunks
delay: 5000,
})
}).as('assemblyEnd')

// Start upload
cy.get('@input').attachFile({
// Fake file of 256MB
fileContent: new Blob([new ArrayBuffer(256 * 1024 * 1024)]),
fileName: 'photos.zip',
mimeType: 'application/zip',
encoding: 'utf8',
lastModified: new Date().getTime(),
})

cy.wait('@init').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')
})

cy.wait('@chunks').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')
cy.get('@progress')
.children('progress')
.should('not.have.value', '0')
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'estimating time left')
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')

cy.wait(1000).then(() => {
getUploader().pause()
})

cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'paused')
cy.get('@pausedListener').should('have.been.calledOnce')

cy.wait(1000).then(() => {
getUploader().start()
})

cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
cy.get('@resumedListener').should('have.been.calledOnce')
})

// Should will retry until success or timeout
cy.get('@assemblyStart', { timeout: 30000 }).should('have.been.calledOnce').then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('be.visible')

cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'assembling')
})

cy.wait('@assemblyEnd', { timeout: 60000 }).then(() => {
cy.get('[data-cy-upload-picker] .upload-picker__progress')
.as('progress')
.should('not.be.visible')
})
})
})
2 changes: 2 additions & 0 deletions cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { mount } from '@cypress/vue2'

// @ts-expect-error Mock window so this is an internal property
window._oc_capabilities = { files: {} }
// @ts-expect-error Mock window so this is an internal property
window._oc_debug = true

// Example use:
// cy.mount(MyComponent)
Expand Down
7 changes: 6 additions & 1 deletion l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ msgstr[0] ""
msgstr[1] ""

msgid "{seconds} seconds left"
msgstr ""
msgid_plural "{seconds} seconds left"
msgstr[0] ""
msgstr[1] ""

#. TRANSLATORS time has the format 00:00:00
msgid "{time} left"
Expand All @@ -31,6 +33,9 @@ msgstr ""
msgid "a few seconds left"
msgstr ""

msgid "assembling"
msgstr ""

msgid "Cancel"
msgstr ""

Expand Down
Loading

0 comments on commit 2f8ce18

Please sign in to comment.