diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-01-31 14:58:03 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-02-05 18:40:00 +0100 |
commit | 5530cdd3fd6af6e45fd8412d5d2ca370729fb5a4 (patch) | |
tree | 05dee9a3ddfc677527517d4f7e7bdfb19dbb9b0d /cypress | |
parent | d9996b92dc2e42961b8eb3343ab7c2c88db12a9f (diff) | |
download | nextcloud-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.ts | 40 | ||||
-rw-r--r-- | cypress/e2e/files/FilesUtils.ts | 44 | ||||
-rw-r--r-- | cypress/e2e/files/files-renaming.cy.ts | 69 | ||||
-rw-r--r-- | cypress/e2e/files/scrolling.cy.ts | 112 |
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) } |