test: Add end-to-end tests for public page header actions

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/47568/head
Ferdinand Thiessen 2024-08-28 17:14:08 +07:00
parent 61d687631b
commit 408c9b2d9d
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
5 changed files with 273 additions and 0 deletions

@ -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`,
]))
})
})

@ -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

@ -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)
}

12
package-lock.json generated

@ -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",

@ -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",