aboutsummaryrefslogtreecommitdiffstats
path: root/cypress
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2025-01-31 14:58:03 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2025-02-05 18:40:00 +0100
commit5530cdd3fd6af6e45fd8412d5d2ca370729fb5a4 (patch)
tree05dee9a3ddfc677527517d4f7e7bdfb19dbb9b0d /cypress
parentd9996b92dc2e42961b8eb3343ab7c2c88db12a9f (diff)
downloadnextcloud-server-5530cdd3fd6af6e45fd8412d5d2ca370729fb5a4.tar.gz
nextcloud-server-5530cdd3fd6af6e45fd8412d5d2ca370729fb5a4.zip
test(files): Make scrolling tests independent from magic values
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'cypress')
-rw-r--r--cypress/e2e/core-utils.ts40
-rw-r--r--cypress/e2e/files/FilesUtils.ts44
-rw-r--r--cypress/e2e/files/files-renaming.cy.ts69
-rw-r--r--cypress/e2e/files/scrolling.cy.ts112
4 files changed, 185 insertions, 80 deletions
diff --git a/cypress/e2e/core-utils.ts b/cypress/e2e/core-utils.ts
index 24053941968..4756836387a 100644
--- a/cypress/e2e/core-utils.ts
+++ b/cypress/e2e/core-utils.ts
@@ -48,3 +48,43 @@ export enum UnifiedSearchFilter {
export function getUnifiedSearchFilter(filter: UnifiedSearchFilter) {
return getUnifiedSearchModal().find(`[data-cy-unified-search-filters] [data-cy-unified-search-filter="${CSS.escape(filter)}"]`)
}
+
+/**
+ * Assertion that an element is fully within the current viewport.
+ * @param $el The element
+ * @param expected If the element is expected to be fully in viewport or not fully
+ * @example
+ * ```js
+ * cy.get('#my-element')
+ * .should(beFullyInViewport)
+ * ```
+ */
+export function beFullyInViewport($el: JQuery<HTMLElement>, expected = true) {
+ const { top, left, bottom, right } = $el.get(0)!.getBoundingClientRect()
+ const innerHeight = Cypress.$('body').innerHeight()!
+ const innerWidth = Cypress.$('body').innerWidth()!
+ const fullyVisible = top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
+
+ console.debug(`fullyVisible: ${fullyVisible}, top: ${top >= 0}, left: ${left >= 0}, bottom: ${bottom <= innerHeight}, right: ${right <= innerWidth}`)
+
+ if (expected) {
+ // eslint-disable-next-line no-unused-expressions
+ expect(fullyVisible, 'Fully within viewport').to.be.true
+ } else {
+ // eslint-disable-next-line no-unused-expressions
+ expect(fullyVisible, 'Not fully within viewport').to.be.false
+ }
+}
+
+/**
+ * Opposite of `beFullyInViewport` - resolves when element is not or only partially in viewport.
+ * @param $el The element
+ * @example
+ * ```js
+ * cy.get('#my-element')
+ * .should(notBeFullyInViewport)
+ * ```
+ */
+export function notBeFullyInViewport($el: JQuery<HTMLElement>) {
+ return beFullyInViewport($el, false)
+}
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
index 37c2b1eff7e..2c7e2666d0c 100644
--- a/cypress/e2e/files/FilesUtils.ts
+++ b/cypress/e2e/files/FilesUtils.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { User } from "@nextcloud/cypress"
+import type { User } from '@nextcloud/cypress'
export const getRowForFileId = (fileid: number) => cy.get(`[data-cy-files-list-row-fileid="${fileid}"]`)
export const getRowForFile = (filename: string) => cy.get(`[data-cy-files-list-row-name="${CSS.escape(filename)}"]`)
@@ -214,3 +214,45 @@ export const reloadCurrentFolder = () => {
cy.get('[data-cy-files-content-breadcrumbs]').findByRole('button', { description: 'Reload current directory' }).click()
cy.wait('@propfind')
}
+
+/**
+ * Enable the grid mode for the files list.
+ * Will fail if already enabled!
+ */
+export function enableGridMode() {
+ cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
+ cy.findByRole('button', { name: 'Switch to grid view' })
+ .should('be.visible')
+ .click()
+ cy.wait('@setGridMode')
+}
+
+/**
+ * Calculate the needed viewport height to limit the visible rows of the file list.
+ * Requires a logged in user.
+ *
+ * @param rows The number of rows that should be displayed at the same time
+ */
+export function calculateViewportHeight(rows: number): Cypress.Chainable<number> {
+ cy.visit('/apps/files')
+
+ return cy.get('[data-cy-files-list]')
+ .should('be.visible')
+ .then((filesList) => {
+ const windowHeight = Cypress.$('body').outerHeight()!
+ // Size of other page elements
+ const outerHeight = Math.ceil(windowHeight - filesList.outerHeight()!)
+ // Size of before and filters
+ const beforeHeight = Math.ceil(Cypress.$('.files-list__before').outerHeight()!)
+ const filterHeight = Math.ceil(Cypress.$('.files-list__filters').outerHeight()!)
+ // Size of the table header
+ const tableHeaderHeight = Math.ceil(Cypress.$('[data-cy-files-list-thead]').outerHeight()!)
+ // table row height
+ const rowHeight = Math.ceil(Cypress.$('[data-cy-files-list-tbody] tr').outerHeight()!)
+
+ // sum it up
+ const viewportHeight = outerHeight + beforeHeight + filterHeight + tableHeaderHeight + rows * rowHeight
+ cy.log(`Calculated viewport height: ${viewportHeight} (${outerHeight} + ${beforeHeight} + ${filterHeight} + ${tableHeaderHeight} + ${rows} * ${rowHeight})`)
+ return cy.wrap(viewportHeight)
+ })
+}
diff --git a/cypress/e2e/files/files-renaming.cy.ts b/cypress/e2e/files/files-renaming.cy.ts
index 752e8264ceb..061f987a24e 100644
--- a/cypress/e2e/files/files-renaming.cy.ts
+++ b/cypress/e2e/files/files-renaming.cy.ts
@@ -4,7 +4,7 @@
*/
import type { User } from '@nextcloud/cypress'
-import { getRowForFile, haveValidity, renameFile, triggerActionForFile } from './FilesUtils'
+import { calculateViewportHeight, getRowForFile, haveValidity, renameFile, triggerActionForFile } from './FilesUtils'
describe('files: Rename nodes', { testIsolation: true }, () => {
let user: User
@@ -12,7 +12,12 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
beforeEach(() => cy.createRandomUser().then(($user) => {
user = $user
+ // remove welcome file
+ cy.rm(user, '/welcome.txt')
+ // create a file called "file.txt"
cy.uploadContent(user, new Blob([]), 'text/plain', '/file.txt')
+
+ // login and visit files app
cy.login(user)
cy.visit('/apps/files')
}))
@@ -116,34 +121,6 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.should('not.exist')
})
- /**
- * This is a regression test of: https://github.com/nextcloud/server/issues/47438
- * The issue was that the renaming state was not reset when the new name moved the file out of the view of the current files list
- * due to virtual scrolling the renaming state was not changed then by the UI events (as the component was taken out of DOM before any event handling).
- */
- it('correctly resets renaming state', () => {
- for (let i = 1; i <= 20; i++) {
- cy.uploadContent(user, new Blob([]), 'text/plain', `/file${i}.txt`)
- }
- cy.viewport(1200, 500) // 500px is smaller then 20 * 50 which is the place that the files take up
- cy.login(user)
- cy.visit('/apps/files')
-
- 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.be.visible')
- // scroll file list to bottom
- cy.get('[data-cy-files-list]').scrollTo('bottom')
- cy.screenshot()
- // The file is no longer in rename state
- getRowForFile('zzz.txt')
- .should('be.visible')
- .findByRole('textbox', { name: 'Filename' })
- .should('not.exist')
- })
-
it('cancel renaming on esc press', () => {
// All are visible by default
getRowForFile('file.txt').should('be.visible')
@@ -182,4 +159,38 @@ describe('files: Rename nodes', { testIsolation: true }, () => {
.find('input[type="text"]')
.should('not.exist')
})
+
+ /**
+ * This is a regression test of: https://github.com/nextcloud/server/issues/47438
+ * The issue was that the renaming state was not reset when the new name moved the file out of the view of the current files list
+ * due to virtual scrolling the renaming state was not changed then by the UI events (as the component was taken out of DOM before any event handling).
+ */
+ it('correctly resets renaming state', () => {
+ // Create 19 additional files
+ for (let i = 1; i <= 19; i++) {
+ cy.uploadContent(user, new Blob([]), 'text/plain', `/file${i}.txt`)
+ }
+
+ // Calculate and setup a viewport where only the first 4 files are visible, causing 6 rows to be rendered
+ cy.viewport(768, 500)
+ cy.login(user)
+ calculateViewportHeight(4)
+ .then((height) => cy.viewport(768, height))
+
+ cy.visit('/apps/files')
+
+ 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')
+ // scroll file list to bottom
+ cy.get('[data-cy-files-list]').scrollTo('bottom')
+ cy.screenshot()
+ // The file is no longer in rename state
+ getRowForFile('zzz.txt')
+ .should('be.visible')
+ .findByRole('textbox', { name: 'Filename' })
+ .should('not.exist')
+ })
})
diff --git a/cypress/e2e/files/scrolling.cy.ts b/cypress/e2e/files/scrolling.cy.ts
index c4ca4943a40..13f37f71454 100644
--- a/cypress/e2e/files/scrolling.cy.ts
+++ b/cypress/e2e/files/scrolling.cy.ts
@@ -4,11 +4,13 @@
*/
import type { User } from '@nextcloud/cypress'
-import { getRowForFile } from './FilesUtils'
+import { calculateViewportHeight, enableGridMode, getRowForFile } from './FilesUtils.ts'
+import { beFullyInViewport, notBeFullyInViewport } from '../core-utils.ts'
describe('files: Scrolling to selected file in file list', { testIsolation: true }, () => {
const fileIds = new Map<number, string>()
let user: User
+ let viewportHeight: number
before(() => {
cy.createRandomUser().then(($user) => {
@@ -19,12 +21,17 @@ describe('files: Scrolling to selected file in file list', { testIsolation: true
cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`)
.then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString()))
}
+
+ cy.login(user)
+ cy.viewport(1200, 800)
+ // Calculate height to ensure that those 10 elements can not be rendered in one list (only 6 will fit the screen)
+ calculateViewportHeight(6)
+ .then((height) => { viewportHeight = height })
})
})
beforeEach(() => {
- // Adjust height to ensure that those 10 elements can not be rendered in one list
- cy.viewport(1200, 6 * 55 /* rows */ + 55 /* table header */ + 50 /* navigation header */ + 50 /* breadcrumbs */ + 46 /* file filters */)
+ cy.viewport(1200, viewportHeight)
cy.login(user)
})
@@ -64,34 +71,36 @@ describe('files: Scrolling to selected file in file list', { testIsolation: true
.and(beOverlappedByTableHeader)
getRowForFile(`${i + 5}.txt`)
.should('exist')
- .and('be.visible')
.and(notBeFullyInViewport)
})
}
- // this will have half of the footer visible
- it(`correctly scrolls to row 6`, () => {
+ // this will have half of the footer visible and half of the previous element
+ it('correctly scrolls to row 6', () => {
cy.visit(`/apps/files/files/${fileIds.get(6)}`)
// See file is visible
- getRowForFile(`6.txt`)
+ getRowForFile('6.txt')
.should('be.visible')
.and(notBeOverlappedByTableHeader)
// we expect also element 7,8,9,10 visible
- getRowForFile(`10.txt`)
+ getRowForFile('10.txt')
.should('be.visible')
// but not row 5
- getRowForFile(`5.txt`)
+ getRowForFile('5.txt')
.should('exist')
.and(beOverlappedByTableHeader)
// see footer is only shown partly
cy.get('tfoot')
.should('exist')
.and(notBeFullyInViewport)
+ .contains('10 files')
+ .should('be.visible')
})
- // Same kind of tests for partially visible top and bottom
+ // For the last "page" of entries we can not scroll further
+ // so we show all of the last 4 entries
for (let i = 7; i <= 10; i++) {
it(`correctly scrolls to row ${i}`, () => {
cy.visit(`/apps/files/files/${fileIds.get(i)}`)
@@ -101,15 +110,15 @@ describe('files: Scrolling to selected file in file list', { testIsolation: true
.should('be.visible')
.and(notBeOverlappedByTableHeader)
- // there are only max. 3 rows left so also row 6+ should be visible
- getRowForFile(`6.txt`)
+ // there are only max. 4 rows left so also row 6+ should be visible
+ getRowForFile('6.txt')
.should('be.visible')
- getRowForFile(`10.txt`)
+ getRowForFile('10.txt')
.should('be.visible')
// Also the footer is visible
cy.get('tfoot')
.contains('10 files')
- .should('be.visible')
+ .should(beFullyInViewport)
})
}
})
@@ -117,8 +126,13 @@ describe('files: Scrolling to selected file in file list', { testIsolation: true
describe('files: Scrolling to selected file in file list (GRID MODE)', { testIsolation: true }, () => {
const fileIds = new Map<number, string>()
let user: User
+ let viewportHeight: number
before(() => {
+ cy.wrap(Cypress.automation('remote:debugger:protocol', {
+ command: 'Network.clearBrowserCache',
+ }))
+
cy.createRandomUser().then(($user) => {
user = $user
@@ -127,21 +141,22 @@ describe('files: Scrolling to selected file in file list (GRID MODE)', { testIso
cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`)
.then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString()))
}
+
// Set grid mode
cy.login(user)
- cy.intercept('**/apps/files/api/v1/config/grid_view').as('setGridMode')
cy.visit('/apps/files')
- cy.findByRole('button', { name: 'Switch to grid view' })
- .should('be.visible')
- .click()
- cy.wait('@setGridMode')
+ enableGridMode()
+
+ // 768px width will limit the columns to 3
+ cy.viewport(768, 800)
+ // Calculate height to ensure that those 12 elements can not be rendered in one list (only 3 will fit the screen)
+ calculateViewportHeight(3)
+ .then((height) => { viewportHeight = height })
})
})
beforeEach(() => {
- // Adjust height to ensure that those 12 files can not be rendered in one list
- // 768px width will limit the columns to 3
- cy.viewport(768, 3 * 246 /* rows */ + 55 /* table header */ + 50 /* navigation header */ + 50 /* breadcrumbs */ + 46 /* file filters */)
+ cy.viewport(768, viewportHeight)
cy.login(user)
})
@@ -155,13 +170,13 @@ describe('files: Scrolling to selected file in file list (GRID MODE)', { testIso
getRowForFile(`${j}.txt`)
.should('be.visible')
// we expect also the second row to be visible
- getRowForFile(`${j+3}.txt`)
+ getRowForFile(`${j + 3}.txt`)
.should('be.visible')
// Because there is no half row on top we also see the third row
- getRowForFile(`${j+6}.txt`)
+ getRowForFile(`${j + 6}.txt`)
.should('be.visible')
// But not the forth row
- getRowForFile(`${j+9}.txt`)
+ getRowForFile(`${j + 9}.txt`)
.should('exist')
.and(notBeFullyInViewport)
}
@@ -215,8 +230,9 @@ describe('files: Scrolling to selected file in file list (GRID MODE)', { testIso
// see footer is only shown partly
cy.get('tfoot')
- .should('exist')
- .and(notBeFullyInViewport)
+ .should(notBeFullyInViewport)
+ .contains('span', '12 files')
+ .should('be.visible')
})
}
@@ -237,44 +253,40 @@ describe('files: Scrolling to selected file in file list (GRID MODE)', { testIso
// see footer is shown
cy.get('tfoot')
- .should('be.visible')
+ .contains('.files-list__row-name', '12 files')
+ .should(beFullyInViewport)
})
}
})
/// Some helpers
-function notBeOverlappedByTableHeader($el: JQuery<HTMLElement>) {
- return beOverlappedByTableHeader($el, false)
-}
-
+/**
+ * Assert that an element is overlapped by the table header
+ * @param $el The element
+ * @param expected if it should be overlapped or NOT
+ */
function beOverlappedByTableHeader($el: JQuery<HTMLElement>, expected = true) {
const headerRect = Cypress.$('thead').get(0)!.getBoundingClientRect()
const elementRect = $el.get(0)!.getBoundingClientRect()
- const overlap = !(headerRect.right < elementRect.left ||
- headerRect.left > elementRect.right ||
- headerRect.bottom < elementRect.top ||
- headerRect.top > elementRect.bottom)
+ const overlap = !(headerRect.right < elementRect.left
+ || headerRect.left > elementRect.right
+ || headerRect.bottom < elementRect.top
+ || headerRect.top > elementRect.bottom)
if (expected) {
+ // eslint-disable-next-line no-unused-expressions
expect(overlap, 'Overlapped by table header').to.be.true
} else {
+ // eslint-disable-next-line no-unused-expressions
expect(overlap, 'Not overlapped by table header').to.be.false
}
}
-function beFullyInViewport($el: JQuery<HTMLElement>, expected = true) {
- const { top, left, bottom, right } = $el.get(0)!.getBoundingClientRect()
- const { innerHeight, innerWidth } = window
- const fullyVisible = top >= 0 && left >= 0 && bottom <= innerHeight && right <= innerWidth
-
- if (expected) {
- expect(fullyVisible, 'Fully within viewport').to.be.true
- } else {
- expect(fullyVisible, 'Not fully within viewport').to.be.false
- }
-}
-
-function notBeFullyInViewport($el: JQuery<HTMLElement>) {
- return beFullyInViewport($el, false)
+/**
+ * Assert that an element is not overlapped by the table header
+ * @param $el The element
+ */
+function notBeOverlappedByTableHeader($el: JQuery<HTMLElement>) {
+ return beOverlappedByTableHeader($el, false)
}