diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-08-28 17:14:08 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-09-03 16:07:50 +0200 |
commit | 408c9b2d9dd45c9736b03c98c8bd7a67fd547e05 (patch) | |
tree | 9cae4fe3d12a91da561dd2e3cab13e54c1e6275e /cypress | |
parent | 61d687631bcb3780d35ca767a8b38252a488effa (diff) | |
download | nextcloud-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>
Diffstat (limited to 'cypress')
-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 |
3 files changed, 260 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) +} |