Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add assembling status to UploadPicker #1530

Merged
merged 2 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading