Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
83 changes: 43 additions & 40 deletions cypress/e2e/files/FilesUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}"]`))
Comment on lines +19 to +21
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ whereas here we assure a aria-controls is already available (because we do not need to check inline actions) and thus can assume in the then that the previous was retried until available.

}

// eslint-disable-next-line no-unused-expressions
expect(menuButtonId).not.to.be.undefined
Copy link
Contributor Author

@susnux susnux Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ This would just fail and not be retried, so if the menu is not yet loaded (opened) and DOM was updated then this will fail forever in this test run.

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 = () => {
Expand Down Expand Up @@ -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')
}
Expand Down
18 changes: 9 additions & 9 deletions cypress/e2e/files/files-actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,7 +23,6 @@ const expectedDefaultActionsIDs = [
ACTION_COPY_MOVE,
ACTION_DELETE,
ACTION_DETAILS,
ACTION_SHARING_STATUS,
]
const expectedDefaultSelectionActionsIDs = [
ACTION_COPY_MOVE,
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
9 changes: 6 additions & 3 deletions cypress/e2e/files/files-renaming.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
11 changes: 6 additions & 5 deletions cypress/e2e/files_external/files-user-credentials.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand All @@ -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')
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/files_sharing/FilesSharingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
27 changes: 23 additions & 4 deletions cypress/e2e/files_sharing/files-download.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { User } from '@nextcloud/cypress'
import { createShare } from './FilesSharingUtils.ts'
import {
getActionButtonForFile,
getActionEntryForFile,
getRowForFile,
} from '../files/FilesUtils.ts'
Expand Down Expand Up @@ -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')
Expand All @@ -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')
})

Expand All @@ -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')
Expand All @@ -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')
})
})
2 changes: 1 addition & 1 deletion cypress/e2e/files_sharing/note-to-recipient.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
15 changes: 6 additions & 9 deletions cypress/e2e/files_sharing/share-status-action.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }, () => {
/**
Expand Down Expand Up @@ -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')
})

Expand All @@ -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')
})

Expand Down
Loading