diff options
-rw-r--r-- | cypress/e2e/files_sharing/public-share-header-menu.cy.ts | 219 | ||||
-rw-r--r-- | cypress/support/e2e.ts | 1 | ||||
-rw-r--r-- | cypress/support/utils/assertions.ts | 40 | ||||
-rw-r--r-- | package-lock.json | 12 | ||||
-rw-r--r-- | package.json | 1 |
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", |