mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
fix(files): Correctly scroll selected file into view
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
8fd6d8025f
commit
d9996b92dc
4 changed files with 401 additions and 50 deletions
|
|
@ -141,7 +141,7 @@ export default defineComponent({
|
|||
<style scoped lang="scss">
|
||||
// Scoped row
|
||||
tr {
|
||||
margin-bottom: max(25vh, var(--body-container-margin));
|
||||
margin-bottom: var(--body-container-margin);
|
||||
border-top: 1px solid var(--color-border);
|
||||
// Prevent hover effect on the whole row
|
||||
background-color: transparent !important;
|
||||
|
|
|
|||
|
|
@ -536,7 +536,6 @@ export default defineComponent({
|
|||
flex-direction: column;
|
||||
width: 100%;
|
||||
background-color: var(--color-main-background);
|
||||
|
||||
}
|
||||
|
||||
// Table header
|
||||
|
|
@ -853,8 +852,7 @@ export default defineComponent({
|
|||
|
||||
<style lang="scss">
|
||||
// Grid mode
|
||||
tbody.files-list__tbody.files-list__tbody--grid {
|
||||
--half-clickable-area: calc(var(--clickable-area) / 2);
|
||||
.files-list--grid tbody.files-list__tbody {
|
||||
--item-padding: 16px;
|
||||
--icon-preview-size: 166px;
|
||||
--name-height: 32px;
|
||||
|
|
@ -945,7 +943,7 @@ tbody.files-list__tbody.files-list__tbody--grid {
|
|||
|
||||
.files-list__row-actions {
|
||||
position: absolute;
|
||||
inset-inline-end: calc(var(--half-clickable-area) / 2);
|
||||
inset-inline-end: calc(var(--clickable-area) / 4);
|
||||
inset-block-end: calc(var(--mtime-height) / 2);
|
||||
width: var(--clickable-area);
|
||||
height: var(--clickable-area);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<div class="files-list" data-cy-files-list>
|
||||
<div class="files-list"
|
||||
:class="{ 'files-list--grid': gridMode }"
|
||||
data-cy-files-list
|
||||
@scroll.passive="onScroll">
|
||||
<!-- Header -->
|
||||
<div ref="before" class="files-list__before">
|
||||
<slot name="before" />
|
||||
</div>
|
||||
|
||||
<div class="files-list__filters">
|
||||
<div ref="filters" class="files-list__filters">
|
||||
<slot name="filters" />
|
||||
</div>
|
||||
|
||||
|
|
@ -31,7 +34,6 @@
|
|||
<!-- Body -->
|
||||
<tbody :style="tbodyStyle"
|
||||
class="files-list__tbody"
|
||||
:class="gridMode ? 'files-list__tbody--grid' : 'files-list__tbody--list'"
|
||||
data-cy-files-list-tbody>
|
||||
<component :is="dataComponent"
|
||||
v-for="({key, item}, i) in renderedItems"
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
</tbody>
|
||||
|
||||
<!-- Footer -->
|
||||
<tfoot v-show="isReady"
|
||||
<tfoot ref="footer"
|
||||
class="files-list__tfoot"
|
||||
data-cy-files-list-tfoot>
|
||||
<slot name="footer" />
|
||||
|
|
@ -118,6 +120,7 @@ export default defineComponent({
|
|||
return {
|
||||
index: this.scrollToIndex,
|
||||
beforeHeight: 0,
|
||||
footerHeight: 0,
|
||||
headerHeight: 0,
|
||||
tableHeight: 0,
|
||||
resizeObserver: null as ResizeObserver | null,
|
||||
|
|
@ -145,16 +148,33 @@ export default defineComponent({
|
|||
// 166px + 32px (name) + 16px (mtime) + 16px (padding top and bottom)
|
||||
return this.gridMode ? (166 + 32 + 16 + 16 + 16) : 55
|
||||
},
|
||||
|
||||
// Grid mode only
|
||||
itemWidth() {
|
||||
// 166px + 16px x 2 (padding left and right)
|
||||
return 166 + 16 + 16
|
||||
},
|
||||
|
||||
rowCount() {
|
||||
return Math.ceil((this.tableHeight - this.headerHeight) / this.itemHeight) + (this.bufferItems / this.columnCount) * 2 + 1
|
||||
/**
|
||||
* The number of rows currently (fully!) visible
|
||||
*/
|
||||
visibleRows(): number {
|
||||
return Math.floor((this.tableHeight - this.headerHeight) / this.itemHeight)
|
||||
},
|
||||
columnCount() {
|
||||
|
||||
/**
|
||||
* Number of rows that will be rendered.
|
||||
* This includes only visible + buffer rows.
|
||||
*/
|
||||
rowCount(): number {
|
||||
return this.visibleRows + (this.bufferItems / this.columnCount) * 2 + 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Number of columns.
|
||||
* 1 for list view otherwise depending on the file list width.
|
||||
*/
|
||||
columnCount(): number {
|
||||
if (!this.gridMode) {
|
||||
return 1
|
||||
}
|
||||
|
|
@ -217,16 +237,18 @@ export default defineComponent({
|
|||
* The total number of rows that are available
|
||||
*/
|
||||
totalRowCount() {
|
||||
return Math.floor(this.dataSources.length / this.columnCount)
|
||||
return Math.ceil(this.dataSources.length / this.columnCount)
|
||||
},
|
||||
|
||||
tbodyStyle() {
|
||||
const isOverScrolled = this.startIndex + this.rowCount > this.dataSources.length
|
||||
const lastIndex = this.dataSources.length - this.startIndex - this.shownItems
|
||||
const hiddenAfterItems = Math.floor(Math.min(this.dataSources.length - this.startIndex, lastIndex) / this.columnCount)
|
||||
// The number of (virtual) rows above the currently rendered ones.
|
||||
// start index is aligned so this should always be an integer
|
||||
const rowsAbove = Math.round(this.startIndex / this.columnCount)
|
||||
// The number of (virtual) rows below the currently rendered ones.
|
||||
const rowsBelow = Math.max(0, this.totalRowCount - rowsAbove - this.rowCount)
|
||||
|
||||
return {
|
||||
paddingTop: `${Math.floor(this.startIndex / this.columnCount) * this.itemHeight}px`,
|
||||
paddingBottom: isOverScrolled ? 0 : `${hiddenAfterItems * this.itemHeight}px`,
|
||||
paddingBlock: `${rowsAbove * this.itemHeight}px ${rowsBelow * this.itemHeight}px`,
|
||||
minHeight: `${this.totalRowCount * this.itemHeight}px`,
|
||||
}
|
||||
},
|
||||
|
|
@ -238,15 +260,14 @@ export default defineComponent({
|
|||
|
||||
totalRowCount() {
|
||||
if (this.scrollToIndex) {
|
||||
this.$nextTick(() => this.scrollTo(this.scrollToIndex))
|
||||
this.scrollTo(this.scrollToIndex)
|
||||
}
|
||||
},
|
||||
|
||||
columnCount(columnCount, oldColumnCount) {
|
||||
if (oldColumnCount === 0) {
|
||||
// We're initializing, the scroll position
|
||||
// is handled on mounted
|
||||
console.debug('VirtualList: columnCount is 0, skipping scroll')
|
||||
// We're initializing, the scroll position is handled on mounted
|
||||
logger.debug('VirtualList: columnCount is 0, skipping scroll')
|
||||
return
|
||||
}
|
||||
// If the column count changes in grid view,
|
||||
|
|
@ -256,30 +277,28 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
mounted() {
|
||||
const before = this.$refs?.before as HTMLElement
|
||||
const root = this.$el as HTMLElement
|
||||
const thead = this.$refs?.thead as HTMLElement
|
||||
this.$_recycledPool = {} as Record<string, DataSource[DataSourceKey]>
|
||||
|
||||
this.resizeObserver = new ResizeObserver(debounce(() => {
|
||||
this.beforeHeight = before?.clientHeight ?? 0
|
||||
this.headerHeight = thead?.clientHeight ?? 0
|
||||
this.tableHeight = root?.clientHeight ?? 0
|
||||
this.updateHeightVariables()
|
||||
logger.debug('VirtualList: resizeObserver updated')
|
||||
this.onScroll()
|
||||
}, 100, { immediate: false }))
|
||||
}, 100))
|
||||
this.resizeObserver.observe(this.$el)
|
||||
this.resizeObserver.observe(this.$refs.before as HTMLElement)
|
||||
this.resizeObserver.observe(this.$refs.filters as HTMLElement)
|
||||
this.resizeObserver.observe(this.$refs.footer as HTMLElement)
|
||||
|
||||
this.resizeObserver.observe(before)
|
||||
this.resizeObserver.observe(root)
|
||||
this.resizeObserver.observe(thead)
|
||||
|
||||
if (this.scrollToIndex) {
|
||||
this.scrollTo(this.scrollToIndex)
|
||||
}
|
||||
|
||||
// Adding scroll listener AFTER the initial scroll to index
|
||||
this.$el.addEventListener('scroll', this.onScroll, { passive: true })
|
||||
|
||||
this.$_recycledPool = {} as Record<string, DataSource[DataSourceKey]>
|
||||
this.$nextTick(() => {
|
||||
// Make sure height values are initialized
|
||||
this.updateHeightVariables()
|
||||
// If we need to scroll to an index we do so in the next tick.
|
||||
// This is needed to apply updates from the initialization of the height variables
|
||||
// which will update the tbody styles until next tick.
|
||||
if (this.scrollToIndex) {
|
||||
this.scrollTo(this.scrollToIndex)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
|
@ -294,17 +313,56 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
// Check if the content is smaller than the viewport, meaning no scrollbar
|
||||
const targetRow = Math.ceil(this.dataSources.length / this.columnCount)
|
||||
if (targetRow < this.rowCount) {
|
||||
logger.debug('VirtualList: Skip scrolling, nothing to scroll', { index, targetRow, rowCount: this.rowCount })
|
||||
// Check if the content is smaller (not equal! keep the footer in mind) than the viewport
|
||||
// meaning there is no scrollbar
|
||||
if (this.totalRowCount < this.visibleRows) {
|
||||
logger.debug('VirtualList: Skip scrolling, nothing to scroll', {
|
||||
index,
|
||||
totalRows: this.totalRowCount,
|
||||
visibleRows: this.visibleRows,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Scroll to one row and a half before the index
|
||||
const scrollTop = this.indexToScrollPos(index)
|
||||
logger.debug('VirtualList: scrolling to index ' + index, { scrollTop, columnCount: this.columnCount, beforeHeight: this.beforeHeight })
|
||||
this.$el.scrollTop = scrollTop
|
||||
// We can not scroll further as the last page of rows
|
||||
// For the grid view we also need to account for all columns in that row (columnCount - 1)
|
||||
const clampedIndex = (this.totalRowCount - this.visibleRows) * this.columnCount + (this.columnCount - 1)
|
||||
// The scroll position
|
||||
let scrollTop = this.indexToScrollPos(Math.min(index, clampedIndex))
|
||||
|
||||
// First we need to update the internal index for rendering.
|
||||
// This will cause the <tbody> element to be resized allowing us to set the correct scroll position.
|
||||
this.index = index
|
||||
|
||||
// If this is not the first row we can add a half row from above.
|
||||
// This is to help users understand the table is scrolled and not items did not just disappear.
|
||||
// But we also can only add a half row if we have enough rows below to scroll (visual rows / end of scrollable area)
|
||||
if (index >= this.columnCount && index <= clampedIndex) {
|
||||
scrollTop -= (this.itemHeight / 2)
|
||||
// As we render one half row more we also need to adjust the internal index
|
||||
this.index = index - this.columnCount
|
||||
} else if (index > clampedIndex) {
|
||||
// If we are on the last page we cannot scroll any further
|
||||
// but we can at least scroll the footer into view
|
||||
if (index <= (clampedIndex + this.columnCount)) {
|
||||
// We only show have of the footer for the first of the last page
|
||||
// To still show the previous row partly. Same reasoning as above:
|
||||
// help the user understand that the table is scrolled not "magically trimmed"
|
||||
scrollTop += this.footerHeight / 2
|
||||
} else {
|
||||
// We reached the very end of the files list and we are focussing not the first visible row
|
||||
// so all we now can do is scroll to the end (footer)
|
||||
scrollTop += this.footerHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need to wait for the <tbody> element to get resized so we can correctly apply the scrollTop position
|
||||
this.$nextTick(() => {
|
||||
this.$el.scrollTop = scrollTop
|
||||
logger.debug(`VirtualList: scrolling to index ${index}`, {
|
||||
clampedIndex, scrollTop, columnCount: this.columnCount, total: this.totalRowCount, visibleRows: this.visibleRows, beforeHeight: this.beforeHeight,
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
onScroll() {
|
||||
|
|
@ -333,7 +391,22 @@ export default defineComponent({
|
|||
// Convert index to scroll position
|
||||
// It should be the opposite of `scrollPosToIndex`
|
||||
indexToScrollPos(index: number): number {
|
||||
return (Math.floor(index / this.columnCount) - 0.5) * this.itemHeight + this.beforeHeight
|
||||
return Math.floor(index / this.columnCount) * this.itemHeight + this.beforeHeight
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the height variables.
|
||||
* To be called by resize observer and `onMount`
|
||||
*/
|
||||
updateHeightVariables(): void {
|
||||
this.tableHeight = this.$el?.clientHeight ?? 0
|
||||
this.beforeHeight = (this.$refs.before as HTMLElement)?.clientHeight ?? 0
|
||||
this.footerHeight = (this.$refs.footer as HTMLElement)?.clientHeight ?? 0
|
||||
|
||||
// Get the header height which consists of table header and filters
|
||||
const theadHeight = (this.$refs.thead as HTMLElement)?.clientHeight ?? 0
|
||||
const filterHeight = (this.$refs.filters as HTMLElement)?.clientHeight ?? 0
|
||||
this.headerHeight = theadHeight + filterHeight
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
280
cypress/e2e/files/scrolling.cy.ts
Normal file
280
cypress/e2e/files/scrolling.cy.ts
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue