aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-09-05 11:28:17 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-09-06 03:38:48 +0200
commit4a90d5328cae20e913612eec447346b55c2c12ae (patch)
treed032100b7860378c1567bc7dc3f93394c1e8d344
parentbe884eeaec0de15211ca6318f68bdd7a714f3baa (diff)
downloadnextcloud-server-4a90d5328cae20e913612eec447346b55c2c12ae.tar.gz
nextcloud-server-4a90d5328cae20e913612eec447346b55c2c12ae.zip
test: Add end-to-end tests for new public share Vue UI
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--__tests__/mock-window.js6
-rw-r--r--apps/files/src/actions/editLocallyAction.spec.ts3
-rw-r--r--apps/files/src/actions/editLocallyAction.ts2
-rw-r--r--cypress.config.ts13
-rw-r--r--cypress/dockerNode.ts6
-rw-r--r--cypress/e2e/files/FilesUtils.ts10
-rw-r--r--cypress/e2e/files_sharing/FilesSharingUtils.ts11
-rw-r--r--cypress/e2e/files_sharing/file-request.cy.ts44
-rw-r--r--cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts49
-rw-r--r--cypress/e2e/files_sharing/public-share/download-files.cy.ts141
-rw-r--r--cypress/e2e/files_sharing/public-share/header-menu.cy.ts (renamed from cypress/e2e/files_sharing/public-share-header-menu.cy.ts)134
-rw-r--r--cypress/e2e/files_sharing/public-share/rename-files.cy.ts32
-rw-r--r--cypress/e2e/files_sharing/public-share/setup-public-share.ts119
-rw-r--r--cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts169
-rw-r--r--cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts104
-rw-r--r--cypress/e2e/files_sharing/public-share/view_view-only.cy.ts107
-rw-r--r--cypress/support/commands.ts36
-rw-r--r--cypress/support/utils/assertions.ts4
-rw-r--r--package-lock.json2
-rw-r--r--package.json2
-rw-r--r--vitest.config.ts5
21 files changed, 879 insertions, 120 deletions
diff --git a/__tests__/mock-window.js b/__tests__/mock-window.js
index dbb689a4f2c..e0c8607dc22 100644
--- a/__tests__/mock-window.js
+++ b/__tests__/mock-window.js
@@ -2,12 +2,8 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { beforeEach } from 'vitest'
-
window.OC = { ...window.OC }
window.OCA = { ...window.OCA }
window.OCP = { ...window.OCP }
-beforeEach(() => {
- window.location = new URL('http://nextcloud.local')
-})
+window._oc_webroot = ''
diff --git a/apps/files/src/actions/editLocallyAction.spec.ts b/apps/files/src/actions/editLocallyAction.spec.ts
index 4d07bb15189..9c7de1b78be 100644
--- a/apps/files/src/actions/editLocallyAction.spec.ts
+++ b/apps/files/src/actions/editLocallyAction.spec.ts
@@ -120,6 +120,7 @@ describe('Edit locally action execute tests', () => {
data: { ocs: { data: { token: 'foobar' } } },
}))
const showError = vi.spyOn(nextcloudDialogs, 'showError')
+ const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const file = new File({
id: 1,
@@ -138,7 +139,7 @@ describe('Edit locally action execute tests', () => {
expect(axios.post).toBeCalledTimes(1)
expect(axios.post).toBeCalledWith('http://nextcloud.local/ocs/v2.php/apps/files/api/v1/openlocaleditor?format=json', { path: '/foobar.txt' })
expect(showError).toBeCalledTimes(0)
- expect(window.location.href).toBe('nc://open/test@nextcloud.local/foobar.txt?token=foobar')
+ expect(windowOpenSpy).toBeCalledWith('nc://open/test@nextcloud.local/foobar.txt?token=foobar', '_self')
})
test('Edit locally fails and shows error', async () => {
diff --git a/apps/files/src/actions/editLocallyAction.ts b/apps/files/src/actions/editLocallyAction.ts
index 2471eaf40a5..ae35b0ca409 100644
--- a/apps/files/src/actions/editLocallyAction.ts
+++ b/apps/files/src/actions/editLocallyAction.ts
@@ -73,7 +73,7 @@ const openLocalClient = async function(path: string) {
let url = `nc://open/${uid}@` + window.location.host + encodePath(path)
url += '?token=' + result.data.ocs.data.token
- window.location.href = url
+ window.open(url, '_self')
} catch (error) {
showError(t('files', 'Failed to redirect to client'))
}
diff --git a/cypress.config.ts b/cypress.config.ts
index e9b09f2012d..9f5c394b4c6 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -66,6 +66,19 @@ export default defineConfig({
on('task', { removeDirectory })
+ // This allows to store global data (e.g. the name of a snapshot)
+ // because Cypress.env() and other options are local to the current spec file.
+ const data = {}
+ on('task', {
+ setVariable({ key, value }) {
+ data[key] = value
+ return null
+ },
+ getVariable({ key }) {
+ return data[key] ?? null
+ },
+ })
+
// Disable spell checking to prevent rendering differences
on('before:browser:launch', (browser, launchOptions) => {
if (browser.family === 'chromium' && browser.name !== 'electron') {
diff --git a/cypress/dockerNode.ts b/cypress/dockerNode.ts
index a9e8c7c6c45..71644ae7399 100644
--- a/cypress/dockerNode.ts
+++ b/cypress/dockerNode.ts
@@ -147,6 +147,8 @@ export const configureNextcloud = async function() {
// Saving DB state
console.log('├─ Creating init DB snapshot...')
await runExec(container, ['cp', '/var/www/html/data/owncloud.db', '/var/www/html/data/owncloud.db-init'], true)
+ console.log('├─ Creating init data backup...')
+ await runExec(container, ['tar', 'cf', 'data-init.tar', 'admin'], true, undefined, '/var/www/html/data')
console.log('└─ Nextcloud is now ready to use 🎉')
}
@@ -277,9 +279,11 @@ const runExec = async function(
command: string[],
verbose = false,
user = 'www-data',
+ workdir?: string,
): Promise<string> {
const exec = await container.exec({
Cmd: command,
+ WorkingDir: workdir,
AttachStdout: true,
AttachStderr: true,
User: user,
@@ -296,7 +300,7 @@ const runExec = async function(
stream.on('data', str => {
str = str.trim()
// Remove non printable characters
- .replace(/[^\x20-\x7E]+/g, '')
+ .replace(/[^\x0A\x0D\x20-\x7E]+/g, '')
// Remove non alphanumeric leading characters
.replace(/^[^a-z]/gi, '')
output += str
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
index 345b6402f1f..0f2b1154200 100644
--- a/cypress/e2e/files/FilesUtils.ts
+++ b/cypress/e2e/files/FilesUtils.ts
@@ -9,8 +9,8 @@ export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-r
export const getActionsForFileId = (fileid: number) => getRowForFileId(fileid).find('[data-cy-files-list-row-actions]')
export const getActionsForFile = (filename: string) => getRowForFile(filename).find('[data-cy-files-list-row-actions]')
-export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).find('button[aria-label="Actions"]')
-export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).find('button[aria-label="Actions"]')
+export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
+export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
export const triggerActionForFileId = (fileid: number, actionId: string) => {
getActionButtonForFileId(fileid).click()
@@ -34,7 +34,7 @@ export const moveFile = (fileName: string, dirPath: string) => {
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
- cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
+ cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
if (dirPath === '/') {
// select home folder
@@ -65,7 +65,7 @@ export const copyFile = (fileName: string, dirPath: string) => {
cy.get('.file-picker').within(() => {
// intercept the copy so we can wait for it
- cy.intercept('COPY', /\/remote.php\/dav\/files\//).as('copyFile')
+ cy.intercept('COPY', /\/(remote|public)\.php\/dav\/files\//).as('copyFile')
if (dirPath === '/') {
// select home folder
@@ -95,7 +95,7 @@ export const renameFile = (fileName: string, newFileName: string) => {
triggerActionForFile(fileName, 'rename')
// intercept the move so we can wait for it
- cy.intercept('MOVE', /\/remote.php\/dav\/files\//).as('moveFile')
+ cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').clear()
getRowForFile(fileName).find('[data-cy-files-list-row-name] input').type(`${newFileName}{enter}`)
diff --git a/cypress/e2e/files_sharing/FilesSharingUtils.ts b/cypress/e2e/files_sharing/FilesSharingUtils.ts
index ef8cf462a06..6e97a757a15 100644
--- a/cypress/e2e/files_sharing/FilesSharingUtils.ts
+++ b/cypress/e2e/files_sharing/FilesSharingUtils.ts
@@ -170,14 +170,3 @@ export const createFileRequest = (path: string, options: FileRequestOptions = {}
// Close
cy.get('[data-cy-file-request-dialog-controls="finish"]').click()
}
-
-export const enterGuestName = (name: string) => {
- cy.get('[data-cy-public-auth-prompt-dialog]').should('be.visible')
- cy.get('[data-cy-public-auth-prompt-dialog-name]').should('be.visible')
- cy.get('[data-cy-public-auth-prompt-dialog-submit]').should('be.visible')
-
- cy.get('[data-cy-public-auth-prompt-dialog-name]').type(`{selectall}${name}`)
- cy.get('[data-cy-public-auth-prompt-dialog-submit]').click()
-
- cy.get('[data-cy-public-auth-prompt-dialog]').should('not.exist')
-}
diff --git a/cypress/e2e/files_sharing/file-request.cy.ts b/cypress/e2e/files_sharing/file-request.cy.ts
index 7c33594e25c..d109c4c585d 100644
--- a/cypress/e2e/files_sharing/file-request.cy.ts
+++ b/cypress/e2e/files_sharing/file-request.cy.ts
@@ -5,12 +5,31 @@
import type { User } from '@nextcloud/cypress'
import { createFolder, getRowForFile, navigateToFolder } from '../files/FilesUtils'
-import { createFileRequest, enterGuestName } from './FilesSharingUtils'
+import { createFileRequest } from './FilesSharingUtils'
+
+const enterGuestName = (name: string) => {
+ cy.findByRole('dialog', { name: /Upload files to/ })
+ .should('be.visible')
+ .within(() => {
+ cy.findByRole('textbox', { name: 'Nickname' })
+ .should('be.visible')
+
+ cy.findByRole('textbox', { name: 'Nickname' })
+ .type(`{selectall}${name}`)
+
+ cy.findByRole('button', { name: 'Submit name' })
+ .should('be.visible')
+ .click()
+ })
+
+ cy.findByRole('dialog', { name: /Upload files to/ })
+ .should('not.exist')
+}
describe('Files', { testIsolation: true }, () => {
+ const folderName = 'test-folder'
let user: User
let url = ''
- let folderName = 'test-folder'
it('Login with a user and create a file request', () => {
cy.createRandomUser().then((_user) => {
@@ -33,19 +52,22 @@ describe('Files', { testIsolation: true }, () => {
enterGuestName('Guest')
// Check various elements on the page
- cy.get('#public-upload .emptycontent').should('be.visible')
- cy.get('#public-upload h2').contains(`Upload files to ${folderName}`)
- cy.get('#public-upload input[type="file"]').as('fileInput').should('exist')
+ cy.contains(`Upload files to ${folderName}`)
+ .should('be.visible')
+ cy.findByRole('button', { name: 'Upload' })
+ .should('be.visible')
cy.intercept('PUT', '/public.php/dav/files/*/*').as('uploadFile')
// Upload a file
- cy.get('@fileInput').selectFile({
- contents: Cypress.Buffer.from('abcdef'),
- fileName: 'file.txt',
- mimeType: 'text/plain',
- lastModified: Date.now(),
- }, { force: true })
+ cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
+ .should('exist')
+ .selectFile({
+ contents: Cypress.Buffer.from('abcdef'),
+ fileName: 'file.txt',
+ mimeType: 'text/plain',
+ lastModified: Date.now(),
+ }, { force: true })
cy.wait('@uploadFile').its('response.statusCode').should('eq', 201)
})
diff --git a/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts b/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
new file mode 100644
index 00000000000..078ecf747bf
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/copy-move-files.cy.ts
@@ -0,0 +1,49 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { copyFile, getRowForFile, moveFile, navigateToFolder } from '../../files/FilesUtils.ts'
+import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
+
+describe('files_sharing: Public share - copy and move files', { testIsolation: true }, () => {
+
+ beforeEach(() => {
+ setupPublicShare()
+ .then(() => cy.logout())
+ .then(() => cy.visit(getShareUrl()))
+ })
+
+ it('Can copy a file to new folder', () => {
+ getRowForFile('foo.txt').should('be.visible')
+ getRowForFile('subfolder').should('be.visible')
+
+ copyFile('foo.txt', 'subfolder')
+
+ // still visible
+ getRowForFile('foo.txt').should('be.visible')
+ navigateToFolder('subfolder')
+
+ cy.url().should('contain', 'dir=/subfolder')
+ getRowForFile('foo.txt').should('be.visible')
+ getRowForFile('bar.txt').should('be.visible')
+ getRowForFile('subfolder').should('not.exist')
+ })
+
+ it('Can move a file to new folder', () => {
+ getRowForFile('foo.txt').should('be.visible')
+ getRowForFile('subfolder').should('be.visible')
+
+ moveFile('foo.txt', 'subfolder')
+
+ // wait until visible again
+ getRowForFile('subfolder').should('be.visible')
+
+ // file should be moved -> not exist anymore
+ getRowForFile('foo.txt').should('not.exist')
+ navigateToFolder('subfolder')
+
+ cy.url().should('contain', 'dir=/subfolder')
+ getRowForFile('foo.txt').should('be.visible')
+ getRowForFile('subfolder').should('not.exist')
+ })
+})
diff --git a/cypress/e2e/files_sharing/public-share/download-files.cy.ts b/cypress/e2e/files_sharing/public-share/download-files.cy.ts
new file mode 100644
index 00000000000..4e37d1b38ae
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/download-files.cy.ts
@@ -0,0 +1,141 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+// @ts-expect-error The package is currently broken - but works...
+import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
+
+import { zipFileContains } from '../../../support/utils/assertions.ts'
+import { getRowForFile, triggerActionForFile } from '../../files/FilesUtils.ts'
+import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
+
+describe('files_sharing: Public share - downloading files', { testIsolation: true }, () => {
+
+ const shareName = 'shared'
+
+ before(() => setupPublicShare())
+
+ deleteDownloadsFolderBeforeEach()
+
+ beforeEach(() => {
+ cy.logout()
+ cy.visit(getShareUrl())
+ })
+
+ it('Can download all files', () => {
+ getRowForFile('foo.txt').should('be.visible')
+
+ cy.get('[data-cy-files-list]').within(() => {
+ cy.findByRole('checkbox', { name: /Toggle selection for all files/i })
+ .should('exist')
+ .check({ force: true })
+
+ // see that two files are selected
+ cy.contains('2 selected').should('be.visible')
+
+ // click download
+ cy.findByRole('button', { name: 'Download (selected)' })
+ .should('be.visible')
+ .click()
+
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 30)
+ // Check all files are included
+ .and(zipFileContains([
+ 'foo.txt',
+ 'subfolder/',
+ 'subfolder/bar.txt',
+ ]))
+ })
+ })
+
+ it('Can download selected files', () => {
+ getRowForFile('subfolder')
+ .should('be.visible')
+
+ cy.get('[data-cy-files-list]').within(() => {
+ getRowForFile('subfolder')
+ .findByRole('checkbox')
+ .check({ force: true })
+
+ // see that two files are selected
+ cy.contains('1 selected').should('be.visible')
+
+ // click download
+ cy.findByRole('button', { name: 'Download (selected)' })
+ .should('be.visible')
+ .click()
+
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 30)
+ // Check all files are included
+ .and(zipFileContains([
+ 'subfolder/',
+ 'subfolder/bar.txt',
+ ]))
+ })
+ })
+
+ it('Can download folder by action', () => {
+ getRowForFile('subfolder')
+ .should('be.visible')
+
+ cy.get('[data-cy-files-list]').within(() => {
+ triggerActionForFile('subfolder', 'download')
+
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/subfolder.zip`, null, { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 30)
+ // Check all files are included
+ .and(zipFileContains([
+ 'subfolder/',
+ 'subfolder/bar.txt',
+ ]))
+ })
+ })
+
+ it('Can download file by action', () => {
+ getRowForFile('foo.txt')
+ .should('be.visible')
+
+ cy.get('[data-cy-files-list]').within(() => {
+ triggerActionForFile('foo.txt', 'download')
+
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 5)
+ .and('contain', '<content>foo</content>')
+ })
+ })
+
+ it('Can download file by selection', () => {
+ getRowForFile('foo.txt')
+ .should('be.visible')
+
+ cy.get('[data-cy-files-list]').within(() => {
+ getRowForFile('foo.txt')
+ .findByRole('checkbox')
+ .check({ force: true })
+
+ cy.findByRole('button', { name: 'Download (selected)' })
+ .click()
+
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 5)
+ .and('contain', '<content>foo</content>')
+ })
+ })
+})
diff --git a/cypress/e2e/files_sharing/public-share-header-menu.cy.ts b/cypress/e2e/files_sharing/public-share/header-menu.cy.ts
index 020ea410dba..a89ee8eb90e 100644
--- a/cypress/e2e/files_sharing/public-share-header-menu.cy.ts
+++ b/cypress/e2e/files_sharing/public-share/header-menu.cy.ts
@@ -2,67 +2,39 @@
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { haveValidity, zipFileContains } from '../../support/utils/assertions.ts'
-import { openSharingPanel } from './FilesSharingUtils.ts'
-// @ts-expect-error The package is currently broken - but works...
-import { deleteDownloadsFolderBeforeEach } from 'cypress-delete-downloads-folder'
+import { haveValidity, zipFileContains } from '../../../support/utils/assertions.ts'
+import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
+/**
+ * This tests ensures that on public shares the header actions menu correctly works
+ */
describe('files_sharing: Public share - header actions menu', { testIsolation: true }, () => {
- let shareUrl: string
- const shareName = 'to be shared'
-
- before(() => {
- cy.createRandomUser().then(($user) => {
- cy.mkdir($user, `/${shareName}`)
- cy.mkdir($user, `/${shareName}/subfolder`)
- cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/foo.txt`)
- cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/subfolder/bar.txt`)
- cy.login($user)
- // open the files app
- cy.visit('/apps/files')
- // open the sidebar
- openSharingPanel(shareName)
- // create the share
- cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
- cy.findByRole('button', { name: 'Create a new share link' })
- .click()
- // extract the link
- cy.wait('@createShare').should(({ response }) => {
- const { ocs } = response?.body ?? {}
- shareUrl = ocs?.data.url
- expect(shareUrl).to.match(/^http:\/\//)
- })
- })
- })
-
- deleteDownloadsFolderBeforeEach()
-
+ before(() => setupPublicShare())
beforeEach(() => {
cy.logout()
- cy.visit(shareUrl)
+ cy.visit(getShareUrl())
})
it('Can download all files', () => {
- // Check the button
cy.get('header')
- .findByRole('button', { name: 'Download all files' })
+ .findByRole('button', { name: 'Download' })
.should('be.visible')
cy.get('header')
- .findByRole('button', { name: 'Download all files' })
+ .findByRole('button', { name: 'Download' })
.click()
// check a file is downloaded
const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
+ cy.readFile(`${downloadsFolder}/shared.zip`, null, { timeout: 15000 })
.should('exist')
.and('have.length.gt', 30)
// Check all files are included
.and(zipFileContains([
- `${shareName}/`,
- `${shareName}/foo.txt`,
- `${shareName}/subfolder/`,
- `${shareName}/subfolder/bar.txt`,
+ 'shared/',
+ 'shared/foo.txt',
+ 'shared/subfolder/',
+ 'shared/subfolder/bar.txt',
]))
})
@@ -78,12 +50,12 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
cy.findByRole('menu', { name: /More action/i })
.should('be.visible')
// see correct link in item
- cy.findByRole('menuitem', { name: /Direct link/i })
+ cy.findByRole('menuitem', { name: 'Direct link' })
.should('be.visible')
.and('have.attr', 'href')
.then((attribute) => expect(attribute).to.match(/^http:\/\/.+\/download$/))
// see menu closes on click
- cy.findByRole('menuitem', { name: /Direct link/i })
+ cy.findByRole('menuitem', { name: 'Direct link' })
.click()
cy.findByRole('menu', { name: /More actions/i })
.should('not.exist')
@@ -100,7 +72,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
// See the menu
cy.findByRole('menu', { name: /More action/i })
.should('be.visible')
- // see correct item
+ // see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@@ -125,6 +97,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
+ // see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@@ -134,10 +107,11 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.type('user@nextcloud.local')
// intercept request, the request is continued when the promise is resolved
const { promise, resolve } = Promise.withResolvers()
- cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare', async (req) => {
- await promise
- req.reply({ statusCode: 503 })
+ cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare', (request) => {
+ // we need to wait in the onResponse handler as the intercept handler times out otherwise
+ request.on('response', async (response) => { await promise; response.statusCode = 503 })
}).as('createFederatedShare')
+
// create the share
cy.findByRole('button', { name: 'Create share' })
.click()
@@ -161,7 +135,7 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
.findByRole('button', { name: /More actions/i })
.should('be.visible')
.click()
- // see correct item
+ // see correct button
cy.findByRole('menuitem', { name: /Add to your/i })
.should('be.visible')
.click()
@@ -183,37 +157,43 @@ describe('files_sharing: Public share - header actions menu', { testIsolation: t
it('See primary action is moved to menu on small screens', () => {
cy.viewport(490, 490)
// Check the button does not exist
- cy.get('header')
- .should('be.visible')
- .findByRole('button', { name: 'Download all files' })
- .should('not.exist')
- // Open the menu
- cy.get('header')
- .findByRole('button', { name: /More actions/i })
- .should('be.visible')
- .click()
- // See that the button is located in the menu
- cy.findByRole('menuitem', { name: /Download all files/i })
- .should('be.visible')
- // See all other items are also available
+ cy.get('header').within(() => {
+ cy.findByRole('button', { name: 'Direct link' })
+ .should('not.exist')
+ cy.findByRole('button', { name: 'Download' })
+ .should('not.exist')
+ cy.findByRole('button', { name: /Add to your/i })
+ .should('not.exist')
+ // Open the menu
+ cy.findByRole('button', { name: /More actions/i })
+ .should('be.visible')
+ .click()
+ })
+
+ // See correct number of menu item
cy.findByRole('menu', { name: 'More actions' })
.findAllByRole('menuitem')
.should('have.length', 3)
- // Click the button to test the download
- cy.findByRole('menuitem', { name: /Download all files/i })
- .click()
+ cy.findByRole('menu', { name: 'More actions' })
+ .within(() => {
+ // See that download, federated share and direct link are moved to the menu
+ cy.findByRole('menuitem', { name: /^Download/ })
+ .should('be.visible')
+ cy.findByRole('menuitem', { name: /Add to your/i })
+ .should('be.visible')
+ cy.findByRole('menuitem', { name: 'Direct link' })
+ .should('be.visible')
- // check a file is downloaded
- const downloadsFolder = Cypress.config('downloadsFolder')
- cy.readFile(`${downloadsFolder}/${shareName}.zip`, null, { timeout: 15000 })
- .should('exist')
- .and('have.length.gt', 30)
- // Check all files are included
- .and(zipFileContains([
- `${shareName}/`,
- `${shareName}/foo.txt`,
- `${shareName}/subfolder/`,
- `${shareName}/subfolder/bar.txt`,
- ]))
+ // See that direct link works
+ cy.findByRole('menuitem', { name: 'Direct link' })
+ .should('be.visible')
+ .and('have.attr', 'href')
+ .then((attribute) => expect(attribute).to.match(/^http:\/\/.+\/download$/))
+ // See remote share works
+ cy.findByRole('menuitem', { name: /Add to your/i })
+ .should('be.visible')
+ .click()
+ })
+ cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible')
})
})
diff --git a/cypress/e2e/files_sharing/public-share/rename-files.cy.ts b/cypress/e2e/files_sharing/public-share/rename-files.cy.ts
new file mode 100644
index 00000000000..5f2fe00e650
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/rename-files.cy.ts
@@ -0,0 +1,32 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { getRowForFile, haveValidity, triggerActionForFile } from '../../files/FilesUtils.ts'
+import { getShareUrl, setupPublicShare } from './setup-public-share.ts'
+
+describe('files_sharing: Public share - renaming files', { testIsolation: true }, () => {
+
+ beforeEach(() => {
+ setupPublicShare()
+ .then(() => cy.logout())
+ .then(() => cy.visit(getShareUrl()))
+ })
+
+ it('can rename a file', () => {
+ // All are visible by default
+ getRowForFile('foo.txt').should('be.visible')
+
+ triggerActionForFile('foo.txt', 'rename')
+
+ getRowForFile('foo.txt')
+ .findByRole('textbox', { name: 'Filename' })
+ .should('be.visible')
+ .type('{selectAll}other.txt')
+ .should(haveValidity(''))
+ .type('{enter}')
+
+ // See it is renamed
+ getRowForFile('other.txt').should('be.visible')
+ })
+})
diff --git a/cypress/e2e/files_sharing/public-share/setup-public-share.ts b/cypress/e2e/files_sharing/public-share/setup-public-share.ts
new file mode 100644
index 00000000000..9549552c200
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/setup-public-share.ts
@@ -0,0 +1,119 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { User } from '@nextcloud/cypress'
+import { openSharingPanel } from '../FilesSharingUtils.ts'
+
+let user: User
+let url: string
+
+/**
+ * URL of the share
+ */
+export function getShareUrl() {
+ if (url === undefined) {
+ throw new Error('You need to setup the share first!')
+ }
+ return url
+}
+
+/**
+ * Setup the available data
+ * @param shareName The name of the shared folder
+ */
+function setupData(shareName: string) {
+ cy.mkdir(user, `/${shareName}`)
+ cy.mkdir(user, `/${shareName}/subfolder`)
+ cy.uploadContent(user, new Blob(['<content>foo</content>']), 'text/plain', `/${shareName}/foo.txt`)
+ cy.uploadContent(user, new Blob(['<content>bar</content>']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
+}
+
+/**
+ * Create a public link share
+ * @param shareName The name of the shared folder
+ */
+function createShare(shareName: string) {
+ cy.login(user)
+ // open the files app
+ cy.visit('/apps/files')
+ // open the sidebar
+ openSharingPanel(shareName)
+ // create the share
+ cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
+ cy.findByRole('button', { name: 'Create a new share link' })
+ .click()
+
+ // extract the link
+ return cy.wait('@createShare')
+ .should(({ response }) => {
+ const { ocs } = response!.body
+ url = ocs?.data.url
+ expect(url).to.match(/^http:\/\//)
+ })
+ .then(() => cy.wrap(url))
+}
+
+/**
+ * Adjust share permissions to be editable
+ */
+function adjustSharePermission() {
+ // Update the share to be a file drop
+ cy.findByRole('list', { name: 'Link shares' })
+ .findAllByRole('listitem')
+ .first()
+ .findByRole('button', { name: /Actions/i })
+ .click()
+ cy.findByRole('menuitem', { name: /Customize link/i })
+ .should('be.visible')
+ .click()
+
+ // Enable upload-edit
+ cy.get('[data-cy-files-sharing-share-permissions-bundle]')
+ .should('be.visible')
+ cy.get('[data-cy-files-sharing-share-permissions-bundle="upload-edit"]')
+ .click()
+ // save changes
+ cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
+ cy.findByRole('button', { name: 'Update share' })
+ .click()
+ cy.wait('@updateShare')
+}
+
+/**
+ * Setup a public share and backup the state.
+ * If the setup was already done in another run, the state will be restored.
+ *
+ * @return The URL of the share
+ */
+export function setupPublicShare(): Cypress.Chainable<string> {
+ const shareName = 'shared'
+
+ return cy.task('getVariable', { key: 'public-share-data' })
+ .then((data) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const { dataSnapshot, dbSnapshot, shareUrl } = data as any || {}
+ if (dataSnapshot && dbSnapshot) {
+ cy.restoreDB(dbSnapshot)
+ cy.restoreData(dataSnapshot)
+ url = shareUrl
+ return cy.wrap(shareUrl as string)
+ } else {
+ cy.restoreData()
+ cy.restoreDB()
+
+ const shareData: Record<string, unknown> = {}
+ return cy.createRandomUser()
+ .then(($user) => { user = $user })
+ .then(() => setupData(shareName))
+ .then(() => createShare(shareName))
+ .then((value) => { shareData.shareUrl = value })
+ .then(() => adjustSharePermission())
+ .then(() => cy.backupDB().then((value) => { shareData.dbSnapshot = value }))
+ .then(() => cy.backupData([user.userId]).then((value) => { shareData.dataSnapshot = value }))
+ .then(() => cy.task('setVariable', { key: 'public-share-data', value: shareData }))
+ .then(() => cy.log(`Public share setup, URL: ${shareData.shareUrl}`))
+ .then(() => cy.wrap(url))
+ }
+ })
+}
diff --git a/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts
new file mode 100644
index 00000000000..8bc4b9b8e15
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/view_file-drop.cy.ts
@@ -0,0 +1,169 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { getRowForFile } from '../../files/FilesUtils.ts'
+import { openSharingPanel } from '../FilesSharingUtils.ts'
+
+describe('files_sharing: Public share - File drop', { testIsolation: true }, () => {
+
+ let shareUrl: string
+ let user: string
+ const shareName = 'shared'
+
+ before(() => {
+ cy.createRandomUser().then(($user) => {
+ user = $user.userId
+ cy.mkdir($user, `/${shareName}`)
+ cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
+ cy.login($user)
+ // open the files app
+ cy.visit('/apps/files')
+ // open the sidebar
+ openSharingPanel(shareName)
+ // create the share
+ cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
+ cy.findByRole('button', { name: 'Create a new share link' })
+ .click()
+ // extract the link
+ cy.wait('@createShare').should(({ response }) => {
+ const { ocs } = response?.body ?? {}
+ shareUrl = ocs?.data.url
+ expect(shareUrl).to.match(/^http:\/\//)
+ })
+
+ // Update the share to be a file drop
+ cy.findByRole('list', { name: 'Link shares' })
+ .findAllByRole('listitem')
+ .first()
+ .findByRole('button', { name: /Actions/i })
+ .click()
+ cy.findByRole('menuitem', { name: /Customize link/i })
+ .should('be.visible')
+ .click()
+ cy.get('[data-cy-files-sharing-share-permissions-bundle]')
+ .should('be.visible')
+ cy.get('[data-cy-files-sharing-share-permissions-bundle="file-drop"]')
+ .click()
+
+ // save the update
+ cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
+ cy.findByRole('button', { name: 'Update share' })
+ .click()
+ cy.wait('@updateShare')
+ })
+ })
+
+ beforeEach(() => {
+ cy.logout()
+ cy.visit(shareUrl)
+ })
+
+ it('Cannot see share content', () => {
+ cy.contains(`Upload files to ${shareName}`)
+ .should('be.visible')
+
+ // foo exists
+ cy.userFileExists(user, `${shareName}/foo.txt`).should('be.gt', 0)
+ // but is not visible
+ getRowForFile('foo.txt')
+ .should('not.exist')
+ })
+
+ it('Can only see upload files and upload folders menu entries', () => {
+ cy.contains(`Upload files to ${shareName}`)
+ .should('be.visible')
+
+ cy.findByRole('button', { name: 'New' })
+ .should('be.visible')
+ .click()
+ // See upload actions
+ cy.findByRole('menuitem', { name: 'Upload files' })
+ .should('be.visible')
+ cy.findByRole('menuitem', { name: 'Upload folders' })
+ .should('be.visible')
+ // But no other
+ cy.findByRole('menu')
+ .findAllByRole('menuitem')
+ .should('have.length', 2)
+ })
+
+ it('Can only see dedicated upload button', () => {
+ cy.contains(`Upload files to ${shareName}`)
+ .should('be.visible')
+
+ cy.findByRole('button', { name: 'Upload' })
+ .should('be.visible')
+ .click()
+ // See upload actions
+ cy.findByRole('menuitem', { name: 'Upload files' })
+ .should('be.visible')
+ cy.findByRole('menuitem', { name: 'Upload folders' })
+ .should('be.visible')
+ // But no other
+ cy.findByRole('menu')
+ .findAllByRole('menuitem')
+ .should('have.length', 2)
+ })
+
+ it('Can upload files', () => {
+ cy.contains(`Upload files to ${shareName}`)
+ .should('be.visible')
+
+ const { promise, resolve } = Promise.withResolvers()
+ cy.intercept('PUT', '**/public.php/dav/files/**', (request) => {
+ if (request.url.includes('first.txt')) {
+ // just continue the first one
+ request.continue()
+ } else {
+ // We delay the second one until we checked that the progress bar is visible
+ request.on('response', async () => { await promise })
+ }
+ }).as('uploadFile')
+
+ cy.get('[data-cy-files-sharing-file-drop] input[type="file"]')
+ .should('exist')
+ .selectFile([
+ { fileName: 'first.txt', contents: Buffer.from('8 bytes!') },
+ { fileName: 'second.md', contents: Buffer.from('x'.repeat(128)) },
+ ], { force: true })
+
+ cy.wait('@uploadFile')
+
+ cy.findByRole('progressbar')
+ .should('be.visible')
+ .and((el) => { expect(Number.parseInt(el.attr('value') ?? '0')).be.gte(50) })
+ // continue second request
+ .then(() => resolve(null))
+
+ cy.wait('@uploadFile')
+
+ // Check files uploaded
+ cy.userFileExists(user, `${shareName}/first.txt`).should('eql', 8)
+ cy.userFileExists(user, `${shareName}/second.md`).should('eql', 128)
+ })
+
+ describe('Terms of service', { testIsolation: true }, () => {
+ before(() => cy.runOccCommand('config:app:set --value "TEST: Some disclaimer text" --type string core shareapi_public_link_disclaimertext'))
+ beforeEach(() => cy.visit(shareUrl))
+ after(() => cy.runOccCommand('config:app:delete core shareapi_public_link_disclaimertext'))
+
+ it('shows ToS on file-drop view', () => {
+ cy.contains(`Upload files to ${shareName}`)
+ .should('be.visible')
+ .should('contain.text', 'agree to the terms of service')
+ cy.findByRole('button', { name: /Terms of service/i })
+ .should('be.visible')
+ .click()
+
+ cy.findByRole('dialog', { name: 'Terms of service' })
+ .should('contain.text', 'TEST: Some disclaimer text')
+ // close
+ .findByRole('button', { name: 'Close' })
+ .click()
+
+ cy.findByRole('dialog', { name: 'Terms of service' })
+ .should('not.exist')
+ })
+ })
+})
diff --git a/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts
new file mode 100644
index 00000000000..abcb9ccae62
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/view_view-only-no-download.cy.ts
@@ -0,0 +1,104 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { getActionButtonForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
+import { openSharingPanel } from '../FilesSharingUtils.ts'
+
+describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
+
+ let shareUrl: string
+ const shareName = 'shared'
+
+ before(() => {
+ cy.createRandomUser().then(($user) => {
+ cy.mkdir($user, `/${shareName}`)
+ cy.mkdir($user, `/${shareName}/subfolder`)
+ cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/foo.txt`)
+ cy.uploadContent($user, new Blob([]), 'text/plain', `/${shareName}/subfolder/bar.txt`)
+ cy.login($user)
+ // open the files app
+ cy.visit('/apps/files')
+ // open the sidebar
+ openSharingPanel(shareName)
+ // create the share
+ cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
+ cy.findByRole('button', { name: 'Create a new share link' })
+ .click()
+ // extract the link
+ cy.wait('@createShare').should(({ response }) => {
+ const { ocs } = response?.body ?? {}
+ shareUrl = ocs?.data.url
+ expect(shareUrl).to.match(/^http:\/\//)
+ })
+
+ // Update the share to be a view-only-no-download share
+ cy.findByRole('list', { name: 'Link shares' })
+ .findAllByRole('listitem')
+ .first()
+ .findByRole('button', { name: /Actions/i })
+ .click()
+ cy.findByRole('menuitem', { name: /Customize link/i })
+ .should('be.visible')
+ .click()
+ cy.get('[data-cy-files-sharing-share-permissions-bundle]')
+ .should('be.visible')
+ cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
+ .click()
+ cy.findByRole('checkbox', { name: 'Hide download' })
+ .check({ force: true })
+ // save the update
+ cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
+ cy.findByRole('button', { name: 'Update share' })
+ .click()
+ cy.wait('@updateShare')
+ })
+ })
+
+ beforeEach(() => {
+ cy.logout()
+ cy.visit(shareUrl)
+ })
+
+ it('Can see the files list', () => {
+ // foo exists
+ getRowForFile('foo.txt')
+ .should('be.visible')
+ })
+
+ it('But no actions available', () => {
+ // foo exists
+ getRowForFile('foo.txt')
+ .should('be.visible')
+ // but no actions
+ getActionButtonForFile('foo.txt')
+ .should('not.exist')
+
+ // TODO: We really need Viewer in the server repo.
+ // So we could at least test viewing images
+ })
+
+ it('Can navigate to subfolder', () => {
+ getRowForFile('subfolder')
+ .should('be.visible')
+
+ navigateToFolder('subfolder')
+
+ getRowForFile('bar.txt')
+ .should('be.visible')
+
+ // but also no actions
+ getActionButtonForFile('bar.txt')
+ .should('not.exist')
+ })
+
+ it('Cannot upload files', () => {
+ // wait for file list to be ready
+ getRowForFile('foo.txt')
+ .should('be.visible')
+
+ cy.contains('button', 'New')
+ .should('be.visible')
+ .and('be.disabled')
+ })
+})
diff --git a/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts
new file mode 100644
index 00000000000..4a8aa6b89a3
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share/view_view-only.cy.ts
@@ -0,0 +1,107 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { getActionsForFile, getRowForFile, navigateToFolder } from '../../files/FilesUtils.ts'
+import { openSharingPanel } from '../FilesSharingUtils.ts'
+
+describe('files_sharing: Public share - View only', { testIsolation: true }, () => {
+
+ let shareUrl: string
+ const shareName = 'shared'
+
+ before(() => {
+ cy.createRandomUser().then(($user) => {
+ cy.mkdir($user, `/${shareName}`)
+ cy.mkdir($user, `/${shareName}/subfolder`)
+ cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/foo.txt`)
+ cy.uploadContent($user, new Blob(['content']), 'text/plain', `/${shareName}/subfolder/bar.txt`)
+ cy.login($user)
+ // open the files app
+ cy.visit('/apps/files')
+ // open the sidebar
+ openSharingPanel(shareName)
+ // create the share
+ cy.intercept('POST', '**/ocs/v2.php/apps/files_sharing/api/v1/shares').as('createShare')
+ cy.findByRole('button', { name: 'Create a new share link' })
+ .click()
+ // extract the link
+ cy.wait('@createShare').should(({ response }) => {
+ const { ocs } = response?.body ?? {}
+ shareUrl = ocs?.data.url
+ expect(shareUrl).to.match(/^http:\/\//)
+ })
+
+ // Update the share to be a view-only-no-download share
+ cy.findByRole('list', { name: 'Link shares' })
+ .findAllByRole('listitem')
+ .first()
+ .findByRole('button', { name: /Actions/i })
+ .click()
+ cy.findByRole('menuitem', { name: /Customize link/i })
+ .should('be.visible')
+ .click()
+ cy.get('[data-cy-files-sharing-share-permissions-bundle]')
+ .should('be.visible')
+ cy.get('[data-cy-files-sharing-share-permissions-bundle="read-only"]')
+ .click()
+ // save the update
+ cy.intercept('PUT', '**/ocs/v2.php/apps/files_sharing/api/v1/shares/*').as('updateShare')
+ cy.findByRole('button', { name: 'Update share' })
+ .click()
+ cy.wait('@updateShare')
+ })
+ })
+
+ beforeEach(() => {
+ cy.logout()
+ cy.visit(shareUrl)
+ })
+
+ it('Can see the files list', () => {
+ // foo exists
+ getRowForFile('foo.txt')
+ .should('be.visible')
+ })
+
+ it('Can navigate to subfolder', () => {
+ getRowForFile('subfolder')
+ .should('be.visible')
+
+ navigateToFolder('subfolder')
+
+ getRowForFile('bar.txt')
+ .should('be.visible')
+ })
+
+ it('Cannot upload files', () => {
+ // wait for file list to be ready
+ getRowForFile('foo.txt')
+ .should('be.visible')
+
+ cy.contains('button', 'New')
+ .should('be.visible')
+ .and('be.disabled')
+ })
+
+ it('Only download action is actions available', () => {
+ getActionsForFile('foo.txt')
+ .should('be.visible')
+ .click()
+
+ // Only the download action
+ cy.findByRole('menuitem', { name: 'Download' })
+ .should('be.visible')
+ cy.findAllByRole('menuitem')
+ .should('have.length', 1)
+
+ // Can download
+ cy.findByRole('menuitem', { name: 'Download' }).click()
+ // check a file is downloaded
+ const downloadsFolder = Cypress.config('downloadsFolder')
+ cy.readFile(`${downloadsFolder}/foo.txt`, 'utf-8', { timeout: 15000 })
+ .should('exist')
+ .and('have.length.gt', 5)
+ .and('contain', 'content')
+ })
+})
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 1574a03705f..23f93ea14d9 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -66,6 +66,8 @@ declare global {
*/
runOccCommand(command: string, options?: Partial<Cypress.ExecOptions>): Cypress.Chainable<Cypress.Exec>,
+ userFileExists(user: string, path: string): Cypress.Chainable<number>
+
/**
* Create a snapshot of the current database
*/
@@ -75,7 +77,11 @@ declare global {
* Restore a snapshot of the database
* Default is the post-setup state
*/
- restoreDB(snapshot?: string): Cypress.Chainable,
+ restoreDB(snapshot?: string): Cypress.Chainable
+
+ backupData(users?: string[]): Cypress.Chainable<string>
+
+ restoreData(snapshot?: string): Cypress.Chainable
}
}
}
@@ -85,7 +91,7 @@ Cypress.env('baseUrl', url)
/**
* Enable or disable a user
- * TODO: standardise in @nextcloud/cypress
+ * TODO: standardize in @nextcloud/cypress
*
* @param {User} user the user to dis- / enable
* @param {boolean} enable True if the user should be enable, false to disable
@@ -112,7 +118,7 @@ Cypress.Commands.add('enableUser', (user: User, enable = true) => {
/**
* cy.uploadedFile - uploads a file from the fixtures folder
- * TODO: standardise in @nextcloud/cypress
+ * TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {string} fixture the fixture file name, e.g. image1.jpg
@@ -188,7 +194,7 @@ Cypress.Commands.add('mkdir', (user: User, target: string) => {
/**
* cy.uploadedContent - uploads a raw content
- * TODO: standardise in @nextcloud/cypress
+ * TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {Blob} blob the content to upload
@@ -288,6 +294,13 @@ Cypress.Commands.add('runOccCommand', (command: string, options?: Partial<Cypres
return cy.exec(`docker exec --user www-data ${env} nextcloud-cypress-tests-server php ./occ ${command}`, options)
})
+Cypress.Commands.add('userFileExists', (user: string, path: string) => {
+ user.replaceAll('"', '\\"')
+ path.replaceAll('"', '\\"').replaceAll(/^\/+/gm, '')
+ return cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true })
+ .then((exec) => Number.parseInt(exec.stdout || '0'))
+})
+
Cypress.Commands.add('backupDB', (): Cypress.Chainable<string> => {
const randomString = Math.random().toString(36).substring(7)
cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server cp /var/www/html/data/owncloud.db /var/www/html/data/owncloud.db-${randomString}`)
@@ -299,3 +312,18 @@ Cypress.Commands.add('restoreDB', (snapshot: string = 'init') => {
cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server cp /var/www/html/data/owncloud.db-${snapshot} /var/www/html/data/owncloud.db`)
cy.log(`Restored snapshot ${snapshot}`)
})
+
+Cypress.Commands.add('backupData', (users: string[] = ['admin']) => {
+ const snapshot = Math.random().toString(36).substring(7)
+ const toBackup = users.map((user) => `'${user.replaceAll('\\', '').replaceAll('\'', '\\\'')}'`).join(' ')
+ cy.exec(`docker exec --user www-data rm /var/www/html/data/data-${snapshot}.tar`, { failOnNonZeroExit: false })
+ cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server tar cf /var/www/html/data/data-${snapshot}.tar ${toBackup}`)
+ return cy.wrap(snapshot as string)
+})
+
+Cypress.Commands.add('restoreData', (snapshot?: string) => {
+ snapshot = snapshot ?? 'init'
+ snapshot.replaceAll('\\', '').replaceAll('"', '\\"')
+ cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server rm -vfr $(tar --exclude='*/*' -tf '/var/www/html/data/data-${snapshot}.tar')`)
+ cy.exec(`docker exec --user www-data --workdir /var/www/html/data nextcloud-cypress-tests-server tar -xf '/var/www/html/data/data-${snapshot}.tar'`)
+})
diff --git a/cypress/support/utils/assertions.ts b/cypress/support/utils/assertions.ts
index 8e5acbd306a..08b93b32e86 100644
--- a/cypress/support/utils/assertions.ts
+++ b/cypress/support/utils/assertions.ts
@@ -17,9 +17,9 @@ export function zipFileContains(expectedFiles: string[]) {
const blob = new Blob([buffer])
const zip = new ZipReader(blob.stream())
// check the real file names
- const entries = (await zip.getEntries()).map((e) => e.filename)
+ const entries = (await zip.getEntries()).map((e) => e.filename).sort()
console.info('Zip contains entries:', entries)
- expect(entries).to.deep.equal(expectedFiles)
+ expect(entries).to.deep.equal(expectedFiles.sort())
}
}
diff --git a/package-lock.json b/package-lock.json
index b186c8e27d6..affc85c6361 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
"@nextcloud/moment": "^1.3.1",
"@nextcloud/password-confirmation": "^5.1.1",
"@nextcloud/paths": "^2.2.1",
- "@nextcloud/router": "^3.0.0",
+ "@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.2.3",
"@nextcloud/upload": "^1.6.0",
"@nextcloud/vue": "^8.17.1",
diff --git a/package.json b/package.json
index 40ac5765242..27f34132c8f 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@nextcloud/moment": "^1.3.1",
"@nextcloud/password-confirmation": "^5.1.1",
"@nextcloud/paths": "^2.2.1",
- "@nextcloud/router": "^3.0.0",
+ "@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.2.3",
"@nextcloud/upload": "^1.6.0",
"@nextcloud/vue": "^8.17.1",
diff --git a/vitest.config.ts b/vitest.config.ts
index e58902789b3..cdf322223bd 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -10,6 +10,11 @@ export default defineConfig({
test: {
include: ['{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
+ environmentOptions: {
+ jsdom: {
+ url: 'http://nextcloud.local',
+ },
+ },
coverage: {
include: ['apps/*/src/**', 'core/src/**'],
exclude: ['**.spec.*', '**.test.*', '**.cy.*', 'core/src/tests/**'],