diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-01-31 00:00:05 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-02-05 18:40:00 +0100 |
commit | d9996b92dc2e42961b8eb3343ab7c2c88db12a9f (patch) | |
tree | 66aa780b74d2d5c870bde7352872cea2d3f4655b /cypress | |
parent | 8fd6d8025ffbf1c0b798babc08d79718afdc25db (diff) | |
download | nextcloud-server-d9996b92dc2e42961b8eb3343ab7c2c88db12a9f.tar.gz nextcloud-server-d9996b92dc2e42961b8eb3343ab7c2c88db12a9f.zip |
fix(files): Correctly scroll selected file into view
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'cypress')
-rw-r--r-- | cypress/e2e/files/scrolling.cy.ts | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/cypress/e2e/files/scrolling.cy.ts b/cypress/e2e/files/scrolling.cy.ts new file mode 100644 index 00000000000..c4ca4943a40 --- /dev/null +++ b/cypress/e2e/files/scrolling.cy.ts @@ -0,0 +1,280 @@ +/*! + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { User } from '@nextcloud/cypress' +import { getRowForFile } from './FilesUtils' + +describe('files: Scrolling to selected file in file list', { testIsolation: true }, () => { + const fileIds = new Map<number, string>() + let user: User + + before(() => { + cy.createRandomUser().then(($user) => { + user = $user + + cy.rm(user, '/welcome.txt') + for (let i = 1; i <= 10; i++) { + cy.uploadContent(user, new Blob([]), 'text/plain', `/${i}.txt`) + .then((response) => fileIds.set(i, Number.parseInt(response.headers['oc-fileid']).toString())) + } + }) + }) + + 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.login(user) + }) + + it('Can see first file in list', () => { + cy.visit(`/apps/files/files/${fileIds.get(1)}`) + + // See file is visible + getRowForFile('1.txt') + .should('be.visible') + + // we expect also element 6 to be visible + getRowForFile('6.txt') + .should('be.visible') + // but not element 7 - though it should exist (be buffered) + getRowForFile('7.txt') + .should('exist') + .and('not.be.visible') + }) + + // Same kind of tests for partially visible top and bottom + for (let i = 2; i <= 5; i++) { + it(`correctly scrolls to row ${i}`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + // See file is visible + getRowForFile(`${i}.txt`) + .should('be.visible') + .and(notBeOverlappedByTableHeader) + + // we expect also element +4 to be visible + // (6 visible rows -> 5 without our scrolled row -> so we only have 4 fully visible others + two 1/2 hidden rows) + getRowForFile(`${i + 4}.txt`) + .should('be.visible') + // but not element -1 or +5 - though it should exist (be buffered) + getRowForFile(`${i - 1}.txt`) + .should('exist') + .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`, () => { + cy.visit(`/apps/files/files/${fileIds.get(6)}`) + + // See file is visible + getRowForFile(`6.txt`) + .should('be.visible') + .and(notBeOverlappedByTableHeader) + + // we expect also element 7,8,9,10 visible + getRowForFile(`10.txt`) + .should('be.visible') + // but not row 5 + getRowForFile(`5.txt`) + .should('exist') + .and(beOverlappedByTableHeader) + // see footer is only shown partly + cy.get('tfoot') + .should('exist') + .and(notBeFullyInViewport) + }) + + // Same kind of tests for partially visible top and bottom + for (let i = 7; i <= 10; i++) { + it(`correctly scrolls to row ${i}`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + // See file is visible + getRowForFile(`${i}.txt`) + .should('be.visible') + .and(notBeOverlappedByTableHeader) + + // there are only max. 3 rows left so also row 6+ should be visible + getRowForFile(`6.txt`) + .should('be.visible') + getRowForFile(`10.txt`) + .should('be.visible') + // Also the footer is visible + cy.get('tfoot') + .contains('10 files') + .should('be.visible') + }) + } +}) + +describe('files: Scrolling to selected file in file list (GRID MODE)', { testIsolation: true }, () => { + const fileIds = new Map<number, string>() + let user: User + + before(() => { + cy.createRandomUser().then(($user) => { + user = $user + + cy.rm(user, '/welcome.txt') + for (let i = 1; i <= 12; i++) { + 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') + }) + }) + + 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.login(user) + }) + + // First row + for (let i = 1; i <= 3; i++) { + it(`Can see files in first row (file ${i})`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + for (let j = 1; j <= 3; j++) { + // See all files of that row are visible + getRowForFile(`${j}.txt`) + .should('be.visible') + // we expect also the second row to be visible + 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`) + .should('be.visible') + // But not the forth row + getRowForFile(`${j+9}.txt`) + .should('exist') + .and(notBeFullyInViewport) + } + }) + } + + // Second row + // Same kind of tests for partially visible top and bottom + for (let i = 4; i <= 6; i++) { + it(`correctly scrolls to second row (file ${i})`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + // See all three files of that row are visible + for (let j = 4; j <= 6; j++) { + getRowForFile(`${j}.txt`) + .should('be.visible') + .and(notBeOverlappedByTableHeader) + // we expect also the next row to be visible + getRowForFile(`${j + 3}.txt`) + .should('be.visible') + // but not the row below (should be half cut) + getRowForFile(`${j + 6}.txt`) + .should('exist') + .and(notBeFullyInViewport) + // Same for the row above + getRowForFile(`${j - 3}.txt`) + .should('exist') + .and(beOverlappedByTableHeader) + } + }) + } + + // Third row + // this will have half of the footer visible and half of the previous row + for (let i = 7; i <= 9; i++) { + it(`correctly scrolls to third row (file ${i})`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + // See all three files of that row are visible + for (let j = 7; j <= 9; j++) { + getRowForFile(`${j}.txt`) + .should('be.visible') + // we expect also the next row to be visible + getRowForFile(`${j + 3}.txt`) + .should('be.visible') + // but not the row above + getRowForFile(`${j - 3}.txt`) + .should('exist') + .and(beOverlappedByTableHeader) + } + + // see footer is only shown partly + cy.get('tfoot') + .should('exist') + .and(notBeFullyInViewport) + }) + } + + // Forth row which only has row 4 and 3 visible and the full footer + for (let i = 10; i <= 12; i++) { + it(`correctly scrolls to forth row (file ${i})`, () => { + cy.visit(`/apps/files/files/${fileIds.get(i)}`) + + // See all three files of that row are visible + for (let j = 10; j <= 12; j++) { + getRowForFile(`${j}.txt`) + .should('be.visible') + .and(notBeOverlappedByTableHeader) + // we expect also the row above to be visible + getRowForFile(`${j - 3}.txt`) + .should('be.visible') + } + + // see footer is shown + cy.get('tfoot') + .should('be.visible') + }) + } +}) + +/// Some helpers + +function notBeOverlappedByTableHeader($el: JQuery<HTMLElement>) { + return beOverlappedByTableHeader($el, false) +} + +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) + + if (expected) { + expect(overlap, 'Overlapped by table header').to.be.true + } else { + 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) +} |