Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: Add end-to-end tests for public page header actions
Signed-off-by: Ferdinand Thiessen <[email protected]>
  • Loading branch information
susnux committed Sep 3, 2024
commit 408c9b2d9dd45c9736b03c98c8bd7a67fd547e05
219 changes: 219 additions & 0 deletions cypress/e2e/files_sharing/public-share-header-menu.cy.ts
Original file line number Diff line number Diff line change
@@ -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('[email protected]')
// 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('[email protected]')
// 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`,
]))
})
})
1 change: 1 addition & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions cypress/support/utils/assertions.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down