aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-08-04 13:41:31 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2025-08-05 23:49:46 +0200
commit88be308b069bf3285fb592d7fc250fb3a95d9051 (patch)
tree1c7f82070654df4eed2413fa447e6fd5e9425d32
parent4ce1980eff6dedc2c413ccee771a8d896b21a5ba (diff)
downloadnextcloud-server-test/fix-cypress.tar.gz
nextcloud-server-test/fix-cypress.zip
test(cypress): split helpers for files actions to make tests less flakytest/fix-cypress
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--cypress/e2e/files/FilesUtils.ts83
-rw-r--r--cypress/e2e/files/files-actions.cy.ts18
-rw-r--r--cypress/e2e/files/files-renaming.cy.ts9
-rw-r--r--cypress/e2e/files_external/files-user-credentials.cy.ts11
-rw-r--r--cypress/e2e/files_sharing/FilesSharingUtils.ts4
-rw-r--r--cypress/e2e/files_sharing/files-download.cy.ts27
-rw-r--r--cypress/e2e/files_sharing/note-to-recipient.cy.ts2
-rw-r--r--cypress/e2e/files_sharing/share-status-action.cy.ts15
8 files changed, 96 insertions, 73 deletions
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
index b138d161600..71ea341a7bf 100644
--- a/cypress/e2e/files/FilesUtils.ts
+++ b/cypress/e2e/files/FilesUtils.ts
@@ -15,65 +15,63 @@ export const getActionsForFile = (filename: string) => getRowForFile(filename).f
export const getActionButtonForFileId = (fileid: number) => getActionsForFileId(fileid).findByRole('button', { name: 'Actions' })
export const getActionButtonForFile = (filename: string) => getActionsForFile(filename).findByRole('button', { name: 'Actions' })
-const searchForActionInRow = (row: JQuery<HTMLElement>, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
- const action = row.find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
- if (action.length > 0) {
- cy.log('Found action in row')
- return cy.wrap(action)
- }
-
- // Else look in the action menu
- const menuButtonId = row.find('button[aria-controls]').attr('aria-controls')
- if (menuButtonId === undefined) {
- return cy.wrap(Cypress.$())
- }
+export const getActionEntryForFileId = (fileid: number, actionId: string) => {
+ return getActionButtonForFileId(fileid)
+ .should('have.attr', 'aria-controls')
+ .then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
+}
- // eslint-disable-next-line no-unused-expressions
- expect(menuButtonId).not.to.be.undefined
- return cy.get(`#${menuButtonId} [data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
+export const getActionEntryForFile = (file: string, actionId: string) => {
+ return getActionButtonForFile(file)
+ .should('have.attr', 'aria-controls')
+ .then((menuId) => cy.get(`#${menuId}`).find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`))
}
-export const getActionEntryForFileId = (fileid: number, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
- // If we cannot find the action in the row, it might be in the action menu
- return getRowForFileId(fileid).should('be.visible')
- .then((row) => searchForActionInRow(row, actionId))
+export const getInlineActionEntryForFileId = (fileid: number, actionId: string) => {
+ return getActionsForFileId(fileid)
+ .find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
}
-export const getActionEntryForFile = (filename: string, actionId: string): Cypress.Chainable<JQuery<HTMLElement>> => {
- // If we cannot find the action in the row, it might be in the action menu
- return getRowForFile(filename).should('be.visible')
- .then((row) => searchForActionInRow(row, actionId))
+
+export const getInlineActionEntryForFile = (file: string, actionId: string) => {
+ return getActionsForFile(file)
+ .find(`[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
}
export const triggerActionForFileId = (fileid: number, actionId: string) => {
- // Even if it's inline, we open the action menu to get all actions visible
- getActionButtonForFileId(fileid).click({ force: true })
- // wait for the actions menu to be visible
- cy.findByRole('menu').findAllByRole('menuitem').first().should('be.visible')
- getActionEntryForFileId(fileid, actionId)
- .find('button').last().as('actionButton')
+ getActionButtonForFileId(fileid)
+ .as('actionButton')
.scrollIntoView()
cy.get('@actionButton')
+ .click({ force: true }) // force to avoid issues with overlaying file list header
+ getActionEntryForFileId(fileid, actionId)
+ .find('button')
.should('be.visible')
- .click({ force: true })
+ .click()
}
+
export const triggerActionForFile = (filename: string, actionId: string) => {
- // Even if it's inline, we open the action menu to get all actions visible
- getActionButtonForFile(filename).click({ force: true })
- // wait for the actions menu to be visible
- cy.findByRole('menu').findAllByRole('menuitem').first().should('be.visible')
- getActionEntryForFile(filename, actionId)
- .find('button').last().as('actionButton')
+ getActionButtonForFile(filename)
+ .as('actionButton')
.scrollIntoView()
cy.get('@actionButton')
+ .click({ force: true }) // force to avoid issues with overlaying file list header
+ getActionEntryForFile(filename, actionId)
+ .find('button')
.should('be.visible')
- .click({ force: true })
+ .click()
}
export const triggerInlineActionForFileId = (fileid: number, actionId: string) => {
- getActionsForFileId(fileid).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
+ getActionsForFileId(fileid)
+ .find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
+ .should('exist')
+ .click()
}
export const triggerInlineActionForFile = (filename: string, actionId: string) => {
- getActionsForFile(filename).find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`).should('exist').click()
+ getActionsForFile(filename)
+ .find(`button[data-cy-files-list-row-action="${CSS.escape(actionId)}"]`)
+ .should('exist')
+ .click()
}
export const selectAllFiles = () => {
@@ -176,12 +174,17 @@ export const copyFile = (fileName: string, dirPath: string) => {
export const renameFile = (fileName: string, newFileName: string) => {
getRowForFile(fileName)
+ .should('exist')
+ .scrollIntoView()
+
triggerActionForFile(fileName, 'rename')
// intercept the move so we can wait for it
cy.intercept('MOVE', /\/(remote|public)\.php\/dav\/files\//).as('moveFile')
- getRowForFile(fileName).find('[data-cy-files-list-row-name] input').type(`{selectAll}${newFileName}{enter}`)
+ getRowForFile(fileName)
+ .find('[data-cy-files-list-row-name] input')
+ .type(`{selectAll}${newFileName}{enter}`)
cy.wait('@moveFile')
}
diff --git a/cypress/e2e/files/files-actions.cy.ts b/cypress/e2e/files/files-actions.cy.ts
index a7febbda45a..dbcf810e2a2 100644
--- a/cypress/e2e/files/files-actions.cy.ts
+++ b/cypress/e2e/files/files-actions.cy.ts
@@ -10,7 +10,6 @@ import { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSe
import { ACTION_COPY_MOVE } from '../../../apps/files/src/actions/moveOrCopyAction'
import { ACTION_DELETE } from '../../../apps/files/src/actions/deleteAction'
import { ACTION_DETAILS } from '../../../apps/files/src/actions/sidebarAction'
-import { ACTION_SHARING_STATUS } from '../../../apps/files_sharing/src/files_actions/sharingStatusAction'
declare global {
interface Window {
@@ -24,7 +23,6 @@ const expectedDefaultActionsIDs = [
ACTION_COPY_MOVE,
ACTION_DELETE,
ACTION_DETAILS,
- ACTION_SHARING_STATUS,
]
const expectedDefaultSelectionActionsIDs = [
ACTION_COPY_MOVE,
@@ -90,11 +88,13 @@ describe('Files: Actions', { testIsolation: true }, () => {
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
- }
+ },
})
// Open the menu
- getActionButtonForFileId(fileId).click({ force: true })
+ getActionButtonForFileId(fileId)
+ .scrollIntoView()
+ .click({ force: true })
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
@@ -104,8 +104,8 @@ describe('Files: Actions', { testIsolation: true }, () => {
// Click on the parent action
getActionEntryForFileId(fileId, 'nested-action')
- .find('button').last()
- .should('exist').click({ force: true })
+ .should('be.visible')
+ .click()
// Check we have the children and the back button but not the parent
getActionEntryForFileId(fileId, 'nested-action').should('not.exist')
@@ -115,8 +115,8 @@ describe('Files: Actions', { testIsolation: true }, () => {
// Click on the back button
getActionEntryForFileId(fileId, 'menu-back')
- .find('button').last()
- .should('exist').click({ force: true })
+ .should('be.visible')
+ .click()
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
@@ -177,7 +177,7 @@ describe('Files: Actions', { testIsolation: true }, () => {
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
- }
+ },
})
selectRowForFile('image.jpg')
diff --git a/cypress/e2e/files/files-renaming.cy.ts b/cypress/e2e/files/files-renaming.cy.ts
index d377417de7b..ac1edb1e104 100644
--- a/cypress/e2e/files/files-renaming.cy.ts
+++ b/cypress/e2e/files/files-renaming.cy.ts
@@ -181,13 +181,16 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
cy.visit('/apps/files')
- getRowForFile('file.txt').should('be.visible')
+ getRowForFile('file.txt')
+ .should('be.visible')
// Z so it is shown last
renameFile('file.txt', 'zzz.txt')
// not visible any longer
- getRowForFile('zzz.txt').should('not.exist')
+ getRowForFile('zzz.txt')
+ .should('not.exist')
// scroll file list to bottom
- cy.get('[data-cy-files-list]').scrollTo('bottom')
+ cy.get('[data-cy-files-list]')
+ .scrollTo('bottom')
cy.screenshot()
// The file is no longer in rename state
getRowForFile('zzz.txt')
diff --git a/cypress/e2e/files_external/files-user-credentials.cy.ts b/cypress/e2e/files_external/files-user-credentials.cy.ts
index a0cd805312c..b20b06b69ba 100644
--- a/cypress/e2e/files_external/files-user-credentials.cy.ts
+++ b/cypress/e2e/files_external/files-user-credentials.cy.ts
@@ -5,7 +5,7 @@
import { User } from '@nextcloud/cypress'
import { AuthBackend, createStorageWithConfig, StorageBackend } from './StorageUtils'
-import { getActionEntryForFile, getRowForFile, navigateToFolder, triggerInlineActionForFile } from '../files/FilesUtils'
+import { getInlineActionEntryForFile, getRowForFile, navigateToFolder, triggerInlineActionForFile } from '../files/FilesUtils'
import { ACTION_CREDENTIALS_EXTERNAL_STORAGE } from '../../../apps/files_external/src/actions/enterCredentialsAction'
import { handlePasswordConfirmation } from '../settings/usersUtils'
@@ -72,7 +72,8 @@ describe('Files user credentials', { testIsolation: true }, () => {
// Auth dialog should be closed and the set credentials button should be gone
cy.get('@authDialog').should('not.exist', { timeout: 2000 })
- getActionEntryForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
+ getInlineActionEntryForFile(storageUser.userId, ACTION_CREDENTIALS_EXTERNAL_STORAGE)
+ .should('not.exist')
// Finally, the storage should be accessible
cy.visit('/apps/files')
@@ -112,7 +113,7 @@ describe('Files user credentials', { testIsolation: true }, () => {
// Auth dialog should be closed and the set credentials button should be gone
cy.get('@authDialog').should('not.exist', { timeout: 2000 })
- getActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
+ getInlineActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
// Finally, the storage should be accessible
cy.visit('/apps/files')
@@ -131,8 +132,8 @@ describe('Files user credentials', { testIsolation: true }, () => {
getRowForFile('storage2').should('be.visible')
// Since we already have set the credentials, the action should not be present
- getActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
- getActionEntryForFile('storage2', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
+ getInlineActionEntryForFile('storage1', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
+ getInlineActionEntryForFile('storage2', ACTION_CREDENTIALS_EXTERNAL_STORAGE).should('not.exist')
// Finally, the storage should be accessible
cy.visit('/apps/files')
diff --git a/cypress/e2e/files_sharing/FilesSharingUtils.ts b/cypress/e2e/files_sharing/FilesSharingUtils.ts
index d63e46914c7..c9b30bd576c 100644
--- a/cypress/e2e/files_sharing/FilesSharingUtils.ts
+++ b/cypress/e2e/files_sharing/FilesSharingUtils.ts
@@ -125,8 +125,8 @@ export function updateShare(fileName: string, index: number, shareSettings: Part
export function openSharingPanel(fileName: string) {
triggerActionForFile(fileName, 'details')
- cy.get('#app-sidebar-vue')
- .get('[aria-controls="tab-sharing"]')
+ cy.get('[data-cy-sidebar]')
+ .find('[aria-controls="tab-sharing"]')
.click()
}
diff --git a/cypress/e2e/files_sharing/files-download.cy.ts b/cypress/e2e/files_sharing/files-download.cy.ts
index ce310933ff7..97ea91b7647 100644
--- a/cypress/e2e/files_sharing/files-download.cy.ts
+++ b/cypress/e2e/files_sharing/files-download.cy.ts
@@ -5,6 +5,7 @@
import type { User } from '@nextcloud/cypress'
import { createShare } from './FilesSharingUtils.ts'
import {
+ getActionButtonForFile,
getActionEntryForFile,
getRowForFile,
} from '../files/FilesUtils.ts'
@@ -41,8 +42,13 @@ describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
// visit shared files view
cy.visit('/apps/files')
// see the shared folder
- getRowForFile('folder').should('be.visible')
- getActionEntryForFile('folder', 'download').should('not.exist')
+ getActionButtonForFile('folder')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ // see no download action
+ getActionEntryForFile('folder', 'download')
+ .should('not.exist')
// Disable view without download option
cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
@@ -51,6 +57,10 @@ describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
cy.visit('/apps/files')
// see the shared folder
getRowForFile('folder').should('be.visible')
+ getActionButtonForFile('folder')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
getActionEntryForFile('folder', 'download').should('not.exist')
})
@@ -68,8 +78,13 @@ describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
// visit shared files view
cy.visit('/apps/files')
// see the shared folder
- getRowForFile('file.txt').should('be.visible')
- getActionEntryForFile('file.txt', 'download').should('not.exist')
+ getActionButtonForFile('file.txt')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
+ // see no download action
+ getActionEntryForFile('file.txt', 'download')
+ .should('not.exist')
// Disable view without download option
cy.runOccCommand('config:app:set --value no core shareapi_allow_view_without_download')
@@ -78,6 +93,10 @@ describe('files_sharing: Download forbidden', { testIsolation: true }, () => {
cy.visit('/apps/files')
// see the shared folder
getRowForFile('file.txt').should('be.visible')
+ getActionButtonForFile('file.txt')
+ .should('be.visible')
+ // open the action menu
+ .click({ force: true })
getActionEntryForFile('file.txt', 'download').should('not.exist')
})
})
diff --git a/cypress/e2e/files_sharing/note-to-recipient.cy.ts b/cypress/e2e/files_sharing/note-to-recipient.cy.ts
index 8bca696e95c..08fee587d9a 100644
--- a/cypress/e2e/files_sharing/note-to-recipient.cy.ts
+++ b/cypress/e2e/files_sharing/note-to-recipient.cy.ts
@@ -72,7 +72,7 @@ describe('files_sharing: Note to recipient', { testIsolation: true }, () => {
createShare('folder', sharee.userId, { read: true, download: true, note: 'Hello, this is the note.' })
// reload just to be sure
- cy.reload()
+ cy.visit('/apps/files')
// open the sharing tab
openSharingPanel('folder')
diff --git a/cypress/e2e/files_sharing/share-status-action.cy.ts b/cypress/e2e/files_sharing/share-status-action.cy.ts
index 1b88810047a..c88f117ddc8 100644
--- a/cypress/e2e/files_sharing/share-status-action.cy.ts
+++ b/cypress/e2e/files_sharing/share-status-action.cy.ts
@@ -4,7 +4,7 @@
*/
import type { User } from '@nextcloud/cypress'
import { createShare } from './FilesSharingUtils.ts'
-import { closeSidebar, enableGridMode, getActionButtonForFile, getRowForFile } from '../files/FilesUtils.ts'
+import { closeSidebar, enableGridMode, getActionButtonForFile, getInlineActionEntryForFile, getRowForFile } from '../files/FilesUtils.ts'
describe('files_sharing: Sharing status action', { testIsolation: true }, () => {
/**
@@ -78,10 +78,9 @@ describe('files_sharing: Sharing status action', { testIsolation: true }, () =>
cy.login(user)
cy.visit('/apps/files')
- getRowForFile('folder')
- .should('be.visible')
- .find('[data-cy-files-list-row-actions]')
- .findByRole('button', { name: /^Shared with/i })
+ getInlineActionEntryForFile('folder', 'sharing-status')
+ .should('have.attr', 'aria-label', `Shared with ${sharee.userId}`)
+ .should('have.attr', 'title', `Shared with ${sharee.userId}`)
.should('be.visible')
})
@@ -103,10 +102,8 @@ describe('files_sharing: Sharing status action', { testIsolation: true }, () =>
cy.login(sharee)
cy.visit('/apps/files')
- getRowForFile('folder')
- .should('be.visible')
- .find('[data-cy-files-list-row-actions]')
- .findByRole('button', { name: `Shared by ${user.userId}` })
+ getInlineActionEntryForFile('folder', 'sharing-status')
+ .should('have.attr', 'aria-label', `Shared by ${user.userId}`)
.should('be.visible')
})