aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-08-28 17:14:08 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-09-03 16:07:50 +0200
commit408c9b2d9dd45c9736b03c98c8bd7a67fd547e05 (patch)
tree9cae4fe3d12a91da561dd2e3cab13e54c1e6275e
parent61d687631bcb3780d35ca767a8b38252a488effa (diff)
downloadnextcloud-server-408c9b2d9dd45c9736b03c98c8bd7a67fd547e05.tar.gz
nextcloud-server-408c9b2d9dd45c9736b03c98c8bd7a67fd547e05.zip
test: Add end-to-end tests for public page header actions
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--cypress/e2e/files_sharing/public-share-header-menu.cy.ts219
-rw-r--r--cypress/support/e2e.ts1
-rw-r--r--cypress/support/utils/assertions.ts40
-rw-r--r--package-lock.json12
-rw-r--r--package.json1
5 files changed, 273 insertions, 0 deletions
diff --git a/cypress/e2e/files_sharing/public-share-header-menu.cy.ts b/cypress/e2e/files_sharing/public-share-header-menu.cy.ts
new file mode 100644
index 00000000000..020ea410dba
--- /dev/null
+++ b/cypress/e2e/files_sharing/public-share-header-menu.cy.ts
@@ -0,0 +1,219 @@
+/*!
+ * 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'
+
+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()
+
+ beforeEach(() => {
+ cy.logout()
+ cy.visit(shareUrl)
+ })
+
+ it('Can download all files', () => {
+ // Check the button
+ cy.get('header')
+ .findByRole('button', { name: 'Download all files' })
+ .should('be.visible')
+ cy.get('header')
+ .findByRole('button', { name: 'Download all files' })
+ .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([
+ `${shareName}/`,
+ `${shareName}/foo.txt`,
+ `${shareName}/subfolder/`,
+ `${shareName}/subfolder/bar.txt`,
+ ]))
+ })
+
+ it('Can copy direct link', () => {
+ // Check the button
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .should('be.visible')
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .click()
+ // See the menu
+ cy.findByRole('menu', { name: /More action/i })
+ .should('be.visible')
+ // see correct link in item
+ cy.findByRole('menuitem', { name: /Direct link/i })
+ .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 })
+ .click()
+ cy.findByRole('menu', { name: /More actions/i })
+ .should('not.exist')
+ })
+
+ it('Can create federated share', () => {
+ // Check the button
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .should('be.visible')
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .click()
+ // See the menu
+ cy.findByRole('menu', { name: /More action/i })
+ .should('be.visible')
+ // see correct item
+ cy.findByRole('menuitem', { name: /Add to your/i })
+ .should('be.visible')
+ .click()
+ // see the dialog
+ cy.findByRole('dialog', { name: /Add to your Nextcloud/i })
+ .should('be.visible')
+ cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).within(() => {
+ cy.findByRole('textbox')
+ .type('user@nextcloud.local')
+ // create share
+ cy.intercept('POST', '**/apps/federatedfilesharing/createFederatedShare')
+ .as('createFederatedShare')
+ cy.findByRole('button', { name: 'Create share' })
+ .click()
+ cy.wait('@createFederatedShare')
+ })
+ })
+
+ it('Has user feedback while creating federated share', () => {
+ // Check the button
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .should('be.visible')
+ .click()
+ cy.findByRole('menuitem', { name: /Add to your/i })
+ .should('be.visible')
+ .click()
+ // see the dialog
+ cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => {
+ cy.findByRole('textbox')
+ .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 })
+ }).as('createFederatedShare')
+ // create the share
+ cy.findByRole('button', { name: 'Create share' })
+ .click()
+ // see that while the share is created the button is disabled
+ cy.findByRole('button', { name: 'Create share' })
+ .should('be.disabled')
+ .then(() => {
+ // continue the request
+ resolve(null)
+ })
+ cy.wait('@createFederatedShare')
+ // see that the button is no longer disabled
+ cy.findByRole('button', { name: 'Create share' })
+ .should('not.be.disabled')
+ })
+ })
+
+ it('Has input validation for federated share', () => {
+ // Check the button
+ cy.get('header')
+ .findByRole('button', { name: /More actions/i })
+ .should('be.visible')
+ .click()
+ // see correct item
+ cy.findByRole('menuitem', { name: /Add to your/i })
+ .should('be.visible')
+ .click()
+ // see the dialog
+ cy.findByRole('dialog', { name: /Add to your Nextcloud/i }).should('be.visible').within(() => {
+ // Check domain only
+ cy.findByRole('textbox')
+ .type('nextcloud.local')
+ cy.findByRole('textbox')
+ .should(haveValidity(/user/i))
+ // Check no valid domain
+ cy.findByRole('textbox')
+ .type('{selectAll}user@invalid')
+ cy.findByRole('textbox')
+ .should(haveValidity(/invalid.+url/i))
+ })
+ })
+
+ 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.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()
+
+ // 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`,
+ ]))
+ })
+})
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
index f0299aa7627..65fb4b2a110 100644
--- a/cypress/support/e2e.ts
+++ b/cypress/support/e2e.ts
@@ -6,6 +6,7 @@ import 'cypress-axe'
import './commands.ts'
// Remove with Node 22
+// Ensure that we can use `Promise.withResolvers` - works in browser but on Node we need Node 22+
import 'core-js/actual/promise/with-resolvers.js'
// Fix ResizeObserver loop limit exceeded happening in Cypress only
diff --git a/cypress/support/utils/assertions.ts b/cypress/support/utils/assertions.ts
new file mode 100644
index 00000000000..8e5acbd306a
--- /dev/null
+++ b/cypress/support/utils/assertions.ts
@@ -0,0 +1,40 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { ZipReader } from '@zip.js/zip.js'
+/**
+ * Assert that a file contains a list of expected files
+ * @param expectedFiles List of expected filenames
+ * @example
+ * ```js
+ * cy.readFile('file', null, { ... })
+ * .should(zipFileContains(['file.txt']))
+ * ```
+ */
+export function zipFileContains(expectedFiles: string[]) {
+ return async (buffer: Buffer) => {
+ 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)
+ console.info('Zip contains entries:', entries)
+ expect(entries).to.deep.equal(expectedFiles)
+ }
+}
+
+/**
+ * Check validity of an input element
+ * @param validity The expected validity message (empty string means it is valid)
+ * @example
+ * ```js
+ * cy.findByRole('textbox')
+ * .should(haveValidity(/must not be empty/i))
+ * ```
+ */
+export const haveValidity = (validity: string | RegExp) => {
+ if (typeof validity === 'string') {
+ return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
+ }
+ return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
+}
diff --git a/package-lock.json b/package-lock.json
index bc7ab179691..6828f491109 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -112,6 +112,7 @@
"@vitest/coverage-v8": "^2.0.5",
"@vue/test-utils": "^1.3.5",
"@vue/tsconfig": "^0.5.1",
+ "@zip.js/zip.js": "^2.7.52",
"babel-loader": "^9.1.0",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"babel-plugin-module-resolver": "^5.0.2",
@@ -7079,6 +7080,17 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/@zip.js/zip.js": {
+ "version": "2.7.52",
+ "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz",
+ "integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==",
+ "dev": true,
+ "engines": {
+ "bun": ">=0.7.0",
+ "deno": ">=1.0.0",
+ "node": ">=16.5.0"
+ }
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
diff --git a/package.json b/package.json
index 9591731ac5c..74e60f93888 100644
--- a/package.json
+++ b/package.json
@@ -143,6 +143,7 @@
"@vitest/coverage-v8": "^2.0.5",
"@vue/test-utils": "^1.3.5",
"@vue/tsconfig": "^0.5.1",
+ "@zip.js/zip.js": "^2.7.52",
"babel-loader": "^9.1.0",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"babel-plugin-module-resolver": "^5.0.2",