Skip to content

Commit bab2607

Browse files
authored
Merge pull request #2910 from nextcloud/backport/2860/stable29
[stable29] Support moving versions across storages
2 parents bd47d01 + 107a3cd commit bab2607

10 files changed

+362
-84
lines changed

β€Ž.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_modules/
44
build
55
vendor
66
js/
7+
cypress/downloads/

β€Žcypress.config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export default defineConfig({
2424
// faster video processing
2525
videoCompression: false,
2626

27+
// Prevent elements to be scrolled under a top bar during actions (click, clear, type, etc). Default is 'top'.
28+
// https://github.com/cypress-io/cypress/issues/871
29+
scrollBehavior: 'center',
30+
2731
// Visual regression testing
2832
env: {
2933
failSilently: false,

β€Žcypress/e2e/files/filesUtils.ts

+59-9
Original file line numberDiff line numberDiff line change
@@ -31,33 +31,83 @@ export const triggerActionForFile = (filename: string, actionId: string) => {
3131
cy.get(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"] > button`).should('exist').click()
3232
}
3333

34-
export const moveFile = (fileName: string, dirName: string) => {
34+
export const moveFile = (fileName: string, dirPath: string) => {
3535
getRowForFile(fileName).should('be.visible')
3636
triggerActionForFile(fileName, 'move-copy')
3737

3838
cy.get('.file-picker').within(() => {
3939
// intercept the copy so we can wait for it
40-
cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
40+
cy.intercept({ method: 'MOVE', times: 1, url: /\/remote.php\/dav\/files\// }).as('moveFile')
4141

42-
if (dirName === '/') {
42+
if (dirPath === '/') {
4343
// select home folder
4444
cy.get('button[title="Home"]').should('be.visible').click()
4545
// click move
4646
cy.contains('button', 'Move').should('be.visible').click()
47-
} else if (dirName === '.') {
47+
} else if (dirPath === '.') {
4848
// click move
4949
cy.contains('button', 'Copy').should('be.visible').click()
5050
} else {
51-
// select the folder
52-
cy.get(`[data-filename="${dirName}"]`).should('be.visible').click()
51+
const directories = dirPath.split('/')
52+
directories.forEach((directory) => {
53+
// select the folder
54+
cy.get(`[data-filename="${directory}"]`).should('be.visible').click()
55+
})
56+
5357
// click move
54-
cy.contains('button', `Move to ${dirName}`).should('be.visible').click()
58+
cy.contains('button', `Move to ${directories.at(-1)}`).should('be.visible').click()
5559
}
5660

5761
cy.wait('@moveFile')
5862
})
5963
}
6064

61-
export const navigateToFolder = (folderName: string) => {
62-
getRowForFile(folderName).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
65+
export const copyFile = (fileName: string, dirPath: string) => {
66+
getRowForFile(fileName).should('be.visible')
67+
triggerActionForFile(fileName, 'move-copy')
68+
69+
cy.get('.file-picker').within(() => {
70+
// intercept the copy so we can wait for it
71+
cy.intercept({ method: 'COPY', times: 1, url: /\/remote.php\/dav\/files\// }).as('copyFile')
72+
73+
if (dirPath === '/') {
74+
// select home folder
75+
cy.get('button[title="Home"]').should('be.visible').click()
76+
// click copy
77+
cy.contains('button', 'Copy').should('be.visible').click()
78+
} else if (dirPath === '.') {
79+
// click copy
80+
cy.contains('button', 'Copy').should('be.visible').click()
81+
} else {
82+
const directories = dirPath.split('/')
83+
directories.forEach((directory) => {
84+
// select the folder
85+
cy.get(`[data-filename="${directory}"]`).should('be.visible').click()
86+
})
87+
88+
// click copy
89+
cy.contains('button', `Copy to ${directories.at(-1)}`).should('be.visible').click()
90+
}
91+
92+
cy.wait('@copyFile')
93+
})
94+
}
95+
96+
export const navigateToFolder = (dirPath: string) => {
97+
const directories = dirPath.split('/')
98+
directories.forEach((directory) => {
99+
getRowForFile(directory).should('be.visible').find('[data-cy-files-list-row-name-link]').click()
100+
})
101+
102+
}
103+
104+
export const closeSidebar = () => {
105+
// {force: true} as it might be hidden behind toasts
106+
cy.get('[cy-data-sidebar] .app-sidebar__close').click({ force: true })
107+
}
108+
109+
export const clickOnBreadcumbs = (label: string) => {
110+
cy.intercept({ method: 'PROPFIND', url: /\/remote.php\/dav\// }).as('propfind')
111+
cy.get('[data-cy-files-content-breadcrumbs]').contains(label).click()
112+
cy.wait('@propfind')
63113
}

β€Žcypress/e2e/files_versions/filesVersionsUtils.ts

+9-11
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const uploadThreeVersions = (user: User, fileName: string) => {
3838

3939
export function openVersionsPanel(fileName: string) {
4040
// Detect the versions list fetch
41-
cy.intercept('PROPFIND', '**/dav/versions/*/versions/**').as('getVersions')
41+
cy.intercept({ method: 'PROPFIND', times: 1, url: '**/dav/versions/*/versions/**' }).as('getVersions')
4242

4343
// Open the versions tab
4444
cy.window().then(win => {
@@ -64,20 +64,21 @@ export function triggerVersionAction(index: number, actionName: string) {
6464
}
6565

6666
export function nameVersion(index: number, name: string) {
67-
cy.intercept('PROPPATCH', '**/dav/versions/*/versions/**').as('labelVersion')
67+
cy.intercept({ method: 'PROPPATCH', times: 1, url: '**/dav/versions/*/versions/**' }).as('labelVersion')
6868
triggerVersionAction(index, 'label')
6969
cy.get(':focused').type(`${name}{enter}`)
7070
cy.wait('@labelVersion')
71+
cy.get('.modal-mask').should('not.be.visible')
7172
}
7273

7374
export function restoreVersion(index: number) {
74-
cy.intercept('MOVE', '**/dav/versions/*/versions/**').as('restoreVersion')
75+
cy.intercept({ method: 'MOVE', times: 1, url: '**/dav/versions/*/versions/**' }).as('restoreVersion')
7576
triggerVersionAction(index, 'restore')
7677
cy.wait('@restoreVersion')
7778
}
7879

7980
export function deleteVersion(index: number) {
80-
cy.intercept('DELETE', '**/dav/versions/*/versions/**').as('deleteVersion')
81+
cy.intercept({ method: 'DELETE', times: 1, url: '**/dav/versions/*/versions/**' }).as('deleteVersion')
8182
triggerVersionAction(index, 'delete')
8283
cy.wait('@deleteVersion')
8384
}
@@ -88,12 +89,9 @@ export function doesNotHaveAction(index: number, actionName: string) {
8889
toggleVersionMenu(index)
8990
}
9091

91-
export function assertVersionContent(filename: string, index: number, expectedContent: string) {
92-
const downloadsFolder = Cypress.config('downloadsFolder')
93-
92+
export function assertVersionContent(index: number, expectedContent: string) {
93+
cy.intercept({ method: 'GET', times: 1, url: 'remote.php/**' }).as('downloadVersion')
9494
triggerVersionAction(index, 'download')
95-
96-
return cy.readFile(path.join(downloadsFolder, filename))
97-
.then((versionContent) => expect(versionContent).to.equal(expectedContent))
98-
.then(() => cy.exec(`rm ${downloadsFolder}/${filename}`))
95+
cy.wait('@downloadVersion')
96+
.then(({ response }) => expect(response?.body).to.equal(expectedContent))
9997
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* @copyright Copyright (c) 2024 Louis Chmn <[email protected]>
3+
*
4+
* @author Louis Chmn <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import type { User } from '@nextcloud/cypress'
24+
25+
import { PERMISSION_DELETE, PERMISSION_READ, PERMISSION_WRITE, addUserToGroup, createGroup, createGroupFolder } from '../groupfoldersUtils'
26+
import { assertVersionContent, nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
27+
import { clickOnBreadcumbs, closeSidebar, copyFile, moveFile, navigateToFolder } from '../files/filesUtils'
28+
29+
/**
30+
*
31+
* @param filePath
32+
*/
33+
function assertVersionsContent(filePath: string) {
34+
const path = filePath.split('/').slice(0, -1).join('/')
35+
36+
clickOnBreadcumbs('All files')
37+
38+
if (path !== '') {
39+
navigateToFolder(path)
40+
}
41+
42+
openVersionsPanel(filePath)
43+
44+
cy.get('[data-files-versions-version]').should('have.length', 3)
45+
cy.get('[data-files-versions-version]').eq(2).contains('v1')
46+
assertVersionContent(0, 'v3')
47+
assertVersionContent(1, 'v2')
48+
assertVersionContent(2, 'v1')
49+
closeSidebar()
50+
}
51+
52+
describe('Versions cross storage move', () => {
53+
let randomGroupName: string
54+
let randomGroupFolderName: string
55+
let randomFileName: string
56+
let randomCopiedFileName: string
57+
let user: User
58+
59+
before(() => {
60+
randomGroupName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
61+
randomGroupFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
62+
63+
cy.createRandomUser().then(_user => { user = _user })
64+
createGroup(randomGroupName)
65+
66+
cy.then(() => {
67+
addUserToGroup(randomGroupName, user.userId)
68+
createGroupFolder(randomGroupFolderName, randomGroupName, [PERMISSION_READ, PERMISSION_WRITE, PERMISSION_DELETE])
69+
})
70+
})
71+
72+
beforeEach(() => {
73+
const randomString = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
74+
randomFileName = randomString + '.txt'
75+
randomCopiedFileName = randomString + ' (copy).txt'
76+
uploadThreeVersions(user, `${randomGroupFolderName}/${randomFileName}`)
77+
78+
cy.login(user)
79+
cy.visit('/apps/files')
80+
navigateToFolder(randomGroupFolderName)
81+
openVersionsPanel(`${randomGroupFolderName}/${randomFileName}`)
82+
nameVersion(2, 'v1')
83+
closeSidebar()
84+
})
85+
86+
it('Correctly moves versions to the user\'s FS when the user moves the file out of the groupfolder', () => {
87+
moveFile(randomFileName, '/')
88+
89+
assertVersionsContent(randomFileName)
90+
91+
moveFile(randomFileName, randomGroupFolderName)
92+
93+
assertVersionsContent(`${randomGroupFolderName}/${randomFileName}`)
94+
})
95+
96+
it('Correctly copies versions to the user\'s FS when the user copies the file out of the groupfolder', () => {
97+
copyFile(randomFileName, '/')
98+
99+
assertVersionsContent(randomFileName)
100+
101+
copyFile(randomFileName, randomGroupFolderName)
102+
103+
assertVersionsContent(`${randomGroupFolderName}/${randomCopiedFileName}`)
104+
})
105+
106+
context('When a file is in a subfolder', () => {
107+
let randomSubFolderName
108+
let randomCopiedSubFolderName
109+
let randomSubSubFolderName
110+
111+
beforeEach(() => {
112+
const randomString = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
113+
randomSubFolderName = randomString
114+
randomCopiedSubFolderName = randomString + ' (copy)'
115+
116+
randomSubSubFolderName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
117+
clickOnBreadcumbs('All files')
118+
cy.mkdir(user, `/${randomGroupFolderName}/${randomSubFolderName}`)
119+
cy.mkdir(user, `/${randomGroupFolderName}/${randomSubFolderName}/${randomSubSubFolderName}`)
120+
cy.login(user)
121+
navigateToFolder(randomGroupFolderName)
122+
})
123+
124+
it('Correctly moves versions when user moves the containing folder out of the groupfolder', () => {
125+
moveFile(randomFileName, `${randomSubFolderName}/${randomSubSubFolderName}`)
126+
moveFile(randomSubFolderName, '/')
127+
128+
assertVersionsContent(`${randomSubFolderName}/${randomSubSubFolderName}/${randomFileName}`)
129+
130+
clickOnBreadcumbs('All files')
131+
moveFile(randomSubFolderName, randomGroupFolderName)
132+
133+
assertVersionsContent(`${randomGroupFolderName}/${randomSubFolderName}/${randomSubSubFolderName}/${randomFileName}`)
134+
})
135+
136+
// TODO: re-enable this test when the copy event from groupfolder to the home storage contains the file list.
137+
xit('Correctly copies versions when user copies the containing folder out of the groupfolder', () => {
138+
moveFile(randomFileName, `${randomSubFolderName}/${randomSubSubFolderName}`)
139+
copyFile(randomSubFolderName, '/')
140+
141+
assertVersionsContent(`${randomSubFolderName}/${randomSubSubFolderName}/${randomFileName}`)
142+
143+
clickOnBreadcumbs('All files')
144+
copyFile(randomSubFolderName, randomGroupFolderName)
145+
146+
assertVersionsContent(`${randomGroupFolderName}/${randomCopiedSubFolderName}/${randomSubSubFolderName}/${randomFileName}`)
147+
})
148+
})
149+
})

β€Žcypress/e2e/files_versions/version_download.cy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ describe('Versions download', () => {
5656
})
5757

5858
it('Download versions and assert their content', () => {
59-
assertVersionContent(randomFileName, 0, 'v3')
60-
assertVersionContent(randomFileName, 1, 'v2')
61-
assertVersionContent(randomFileName, 2, 'v1')
59+
assertVersionContent(0, 'v3')
60+
assertVersionContent(1, 'v2')
61+
assertVersionContent(2, 'v1')
6262
})
6363
})

β€Žcypress/e2e/files_versions/version_restoration.cy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ describe('Versions restoration', () => {
7070
})
7171

7272
it('Downloads versions and assert there content', () => {
73-
assertVersionContent(randomFileName, 0, 'v1')
74-
assertVersionContent(randomFileName, 1, 'v3')
75-
assertVersionContent(randomFileName, 2, 'v2')
73+
assertVersionContent(0, 'v1')
74+
assertVersionContent(1, 'v3')
75+
assertVersionContent(2, 'v2')
7676
})
7777
})

β€Žcypress/support/e2e.ts

+5
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@
2020
*
2121
*/
2222
import './commands'
23+
24+
// Fix ResizeObserver loop limit exceeded happening in Cypress only
25+
// @see https://github.com/cypress-io/cypress/issues/20341
26+
Cypress.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop limit exceeded'))
27+
Cypress.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop completed with undelivered notifications'))

0 commit comments

Comments
Β (0)