Skip to content

Commit 2f8ce18

Browse files
authored
Merge pull request #1530 from nextcloud-libraries/feat/assembly
2 parents d4c7eb8 + 5d01576 commit 2f8ce18

File tree

7 files changed

+304
-58
lines changed

7 files changed

+304
-58
lines changed

cypress/components/UploadPicker/UploadPicker.cy.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,6 @@ describe('UploadPicker valid uploads', () => {
138138
afterEach(() => resetDocument())
139139

140140
it('Uploads a file with chunking', () => {
141-
// Init and reset chunk request spy
142-
const chunksRequestsSpy = cy.spy()
143-
144141
// Intercept tmp upload chunks folder creation
145142
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
146143
statusCode: 201,
@@ -151,7 +148,6 @@ describe('UploadPicker valid uploads', () => {
151148
method: 'PUT',
152149
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
153150
}, (req) => {
154-
chunksRequestsSpy()
155151
req.reply({
156152
statusCode: 201,
157153
})
@@ -193,7 +189,7 @@ describe('UploadPicker valid uploads', () => {
193189
cy.get('[data-cy-upload-picker] .upload-picker__progress')
194190
.as('progress')
195191
.should('not.be.visible')
196-
expect(chunksRequestsSpy).to.have.always.been.callCount(26)
192+
cy.get('@chunks.all').should('have.lengthOf', 26)
197193
})
198194
})
199195

cypress/components/UploadPicker/progress.cy.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ describe('UploadPicker: progress handling', () => {
6060
}),
6161
}
6262

63+
// Start paused
64+
getUploader(false, true).pause()
65+
6366
// Mount picker
6467
cy.mount(UploadPicker, {
6568
propsData,
@@ -74,7 +77,6 @@ describe('UploadPicker: progress handling', () => {
7477
it('has increasing progress bar during non-chunked upload', () => {
7578
// Start in paused mode
7679
const uploader = getUploader()
77-
uploader.pause()
7880

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

151153
// Start in paused mode
152154
const uploader = getUploader()
153-
uploader.pause()
154155

155156
// 3 MiB/s meaning upload will take 5 seconds
156157
throttleUpload(3 * 1024 * 1024)
@@ -205,6 +206,74 @@ describe('UploadPicker: progress handling', () => {
205206
cy.get('@progress')
206207
.should('not.be.visible')
207208
})
209+
210+
it('shows the progress bar while assembling', () => {
211+
// Maximum the responses can take
212+
Cypress.config({ defaultCommandTimeout: 7000 })
213+
214+
const { promise, resolve } = Promise.withResolvers<void>()
215+
216+
cy.intercept('PUT', '/remote.php/dav/files/user/file.txt', { statusCode: 201 }).as('upload')
217+
cy.intercept('MKCOL', '/remote.php/dav/uploads/user/*', { statusCode: 201 }).as('mkdir')
218+
cy.intercept('PUT', '/remote.php/dav/uploads/user/*/*', (rq) => {
219+
rq.reply({ statusCode: 201 })
220+
if (rq.url.endsWith('/2')) {
221+
rq.on('response', async () => await promise)
222+
}
223+
}).as('uploadBig')
224+
cy.intercept('MOVE', '/remote.php/dav/uploads/user/*/.file', { statusCode: 201, delay: 1000 }).as('move')
225+
226+
// Start in paused mode
227+
const uploader = getUploader()
228+
229+
cy.get('@input').attachFile([
230+
{
231+
// file of 5 MiB so it is not chunked
232+
fileContent: new Blob([new ArrayBuffer(5 * 1024 * 1024)]),
233+
fileName: 'file.txt',
234+
mimeType: 'text/plain',
235+
encoding: 'utf8',
236+
lastModified: new Date().getTime(),
237+
},
238+
{
239+
// file of 15 MiB so it is chunked in 10MiB and 5 MiB
240+
fileContent: new Blob([new ArrayBuffer(15 * 1024 * 1024)]),
241+
fileName: 'big-file.txt',
242+
mimeType: 'text/plain',
243+
encoding: 'utf8',
244+
lastModified: new Date().getTime(),
245+
},
246+
])
247+
248+
// See there is no progress yet
249+
cy.get('@progress')
250+
.should('be.visible')
251+
.should('have.value', 0)
252+
cy.get('@progressLabel')
253+
.should('contain.text', 'paused')
254+
// start the uploader
255+
.then(() => uploader.start())
256+
257+
// MKCOL was successfully so the upload can begin
258+
cy.wait('@mkdir')
259+
260+
cy.get('@progress', { timeout: 2000 })
261+
.should((el) => expect(el.val()).to.be.greaterThan(10))
262+
.and((el) => expect(el.val()).to.be.lessThan(95))
263+
264+
cy.wait('@upload')
265+
cy.wait('@uploadBig')
266+
.then(() => resolve())
267+
268+
cy.get('@progressLabel')
269+
.should('be.visible')
270+
.and('contain.text', 'assembling')
271+
272+
cy.wait('@move')
273+
274+
cy.get('@progress')
275+
.should('not.be.visible')
276+
})
208277
})
209278

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* eslint-disable no-unused-expressions */
2+
/**
3+
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*/
6+
// dist file might not be built when running eslint only
7+
// eslint-disable-next-line import/no-unresolved,n/no-missing-import
8+
import { Folder, Permission } from '@nextcloud/files'
9+
import { generateRemoteUrl } from '@nextcloud/router'
10+
import { getUploader, UploadPicker } from '../../../lib/index.ts'
11+
12+
let state: string | undefined
13+
before(() => {
14+
cy.window().then((win) => {
15+
state = win.document.body.innerHTML
16+
})
17+
})
18+
19+
const resetDocument = () => {
20+
if (state) {
21+
cy.window().then((win) => {
22+
win.document.body.innerHTML = state!
23+
})
24+
}
25+
}
26+
27+
describe('UploadPicker: status testing', () => {
28+
beforeEach(() => {
29+
// Make sure we reset the destination
30+
// so other tests do not interfere
31+
const propsData = {
32+
destination: new Folder({
33+
id: 56,
34+
owner: 'user',
35+
source: generateRemoteUrl('dav/files/user'),
36+
permissions: Permission.ALL,
37+
root: '/files/user',
38+
}),
39+
}
40+
41+
// Mount picker
42+
const onPause = cy.spy().as('pausedListener')
43+
const onResume = cy.spy().as('resumedListener')
44+
cy.mount(UploadPicker, {
45+
propsData,
46+
listeners: {
47+
paused: onPause,
48+
resumed: onResume,
49+
},
50+
}).as('uploadPicker')
51+
52+
// Check and init aliases
53+
cy.get('[data-cy-upload-picker] [data-cy-upload-picker-input]').as('input').should('exist')
54+
cy.get('[data-cy-upload-picker] .upload-picker__progress').as('progress').should('exist')
55+
})
56+
57+
afterEach(() => resetDocument())
58+
59+
it('shows paused status on pause', () => {
60+
// Intercept tmp upload chunks folder creation
61+
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
62+
statusCode: 201,
63+
}).as('init')
64+
65+
// Intercept chunks upload
66+
cy.intercept({
67+
method: 'PUT',
68+
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
69+
}, (req) => {
70+
req.reply({
71+
statusCode: 201,
72+
})
73+
}).as('chunks')
74+
75+
// Intercept final assembly request
76+
const assemblyStartStub = cy.stub().as('assemblyStart')
77+
cy.intercept('MOVE', '/remote.php/dav/uploads/*/web-file-upload*/.file', (req) => {
78+
assemblyStartStub()
79+
req.reply({
80+
statusCode: 204,
81+
// Fake assembling chunks
82+
delay: 5000,
83+
})
84+
}).as('assemblyEnd')
85+
86+
// Start upload
87+
cy.get('@input').attachFile({
88+
// Fake file of 256MB
89+
fileContent: new Blob([new ArrayBuffer(256 * 1024 * 1024)]),
90+
fileName: 'photos.zip',
91+
mimeType: 'application/zip',
92+
encoding: 'utf8',
93+
lastModified: new Date().getTime(),
94+
})
95+
96+
cy.wait('@init').then(() => {
97+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
98+
.as('progress')
99+
.should('be.visible')
100+
})
101+
102+
cy.wait('@chunks').then(() => {
103+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
104+
.as('progress')
105+
.should('be.visible')
106+
cy.get('@progress')
107+
.children('progress')
108+
.should('not.have.value', '0')
109+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'estimating time left')
110+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
111+
112+
cy.wait(1000).then(() => {
113+
getUploader().pause()
114+
})
115+
116+
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'paused')
117+
cy.get('@pausedListener').should('have.been.calledOnce')
118+
119+
cy.wait(1000).then(() => {
120+
getUploader().start()
121+
})
122+
123+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
124+
cy.get('@resumedListener').should('have.been.calledOnce')
125+
})
126+
127+
// Should will retry until success or timeout
128+
cy.get('@assemblyStart', { timeout: 30000 }).should('have.been.calledOnce').then(() => {
129+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
130+
.as('progress')
131+
.should('be.visible')
132+
133+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
134+
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'assembling')
135+
})
136+
137+
cy.wait('@assemblyEnd', { timeout: 60000 }).then(() => {
138+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
139+
.as('progress')
140+
.should('not.be.visible')
141+
})
142+
})
143+
})

cypress/support/component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { mount } from '@cypress/vue2'
2929

3030
// @ts-expect-error Mock window so this is an internal property
3131
window._oc_capabilities = { files: {} }
32+
// @ts-expect-error Mock window so this is an internal property
33+
window._oc_debug = true
3234

3335
// Example use:
3436
// cy.mount(MyComponent)

l10n/messages.pot

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ msgstr[0] ""
2222
msgstr[1] ""
2323

2424
msgid "{seconds} seconds left"
25-
msgstr ""
25+
msgid_plural "{seconds} seconds left"
26+
msgstr[0] ""
27+
msgstr[1] ""
2628

2729
#. TRANSLATORS time has the format 00:00:00
2830
msgid "{time} left"
@@ -31,6 +33,9 @@ msgstr ""
3133
msgid "a few seconds left"
3234
msgstr ""
3335

36+
msgid "assembling"
37+
msgstr ""
38+
3439
msgid "Cancel"
3540
msgstr ""
3641

0 commit comments

Comments
 (0)