mirror of
https://github.com/nextcloud/server.git
synced 2026-02-21 17:01:34 -05:00
test(files): Make scrolling tests independent from magic values
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
5251b25f4e
commit
b5a3fc58fd
4 changed files with 214 additions and 79 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
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)}"]`)
|
||||
|
||||
|
|
@ -176,3 +178,73 @@ export const haveValidity = (validity: string | RegExp) => {
|
|||
}
|
||||
return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
|
||||
}
|
||||
|
||||
export const deleteFileWithRequest = (user: User, path: string) => {
|
||||
// Ensure path starts with a slash and has no double slashes
|
||||
path = `/${path}`.replace(/\/+/g, '/')
|
||||
|
||||
cy.request('/csrftoken').then(({ body }) => {
|
||||
const requestToken = body.token
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${Cypress.env('baseUrl')}/remote.php/dav/files/${user.userId}` + path,
|
||||
headers: {
|
||||
requestToken,
|
||||
},
|
||||
retryOnStatusCodeFailure: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const triggerFileListAction = (actionId: string) => {
|
||||
cy.get(`button[data-cy-files-list-action="${CSS.escape(actionId)}"]`).last()
|
||||
.should('exist').click({ force: true })
|
||||
}
|
||||
|
||||
export const reloadCurrentFolder = () => {
|
||||
cy.intercept('PROPFIND', /\/remote.php\/dav\//).as('propfind')
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}))
|
||||
|
|
@ -113,34 +118,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')
|
||||
|
|
@ -179,4 +156,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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue