@revoloo/cypress6
Version:
Cypress.io end to end testing tool
138 lines (106 loc) • 3.75 kB
text/typescript
/**
container.clientHeight:
- container visible area height ("viewport")
- includes padding, but not margin or border
container.scrollTop:
- container scroll position:
container.scrollHeight:
- total container height (visible + not visible)
element.clientHeight:
- element height
- includes padding, but not margin or border
element.offsetTop:
- element distance from top of container
*/
import { TimeoutID } from './types'
type UserScrollCallback = () => void
const PADDING = 100
export class Scroller {
private _container: Element | null = null
private _userScrollCount = 0
private _userScroll = true
private _countUserScrollsTimeout?: TimeoutID
setContainer (container: Element, onUserScroll?: UserScrollCallback) {
this._container = container
this._userScroll = true
this._userScrollCount = 0
this._listenToScrolls(onUserScroll)
}
_listenToScrolls (onUserScroll?: UserScrollCallback) {
if (!this._container) return
this._container.addEventListener('scroll', () => {
if (!this._userScroll) {
// programmatic scroll
this._userScroll = true
return
}
// there can be false positives for user scrolls, so make sure we get 3
// or more scroll events within 50ms to count it as a user intending to scroll
this._userScrollCount++
if (this._userScrollCount >= 3) {
if (onUserScroll) {
onUserScroll()
}
clearTimeout(this._countUserScrollsTimeout as TimeoutID)
this._countUserScrollsTimeout = undefined
this._userScrollCount = 0
return
}
if (this._countUserScrollsTimeout) return
this._countUserScrollsTimeout = setTimeout(() => {
this._countUserScrollsTimeout = undefined
this._userScrollCount = 0
}, 50)
})
}
scrollIntoView (element: HTMLElement) {
if (!this._container) {
throw new Error('A container must be set on the scroller with `scroller.setContainer(container)` before trying to scroll an element into view')
}
if (this._isFullyVisible(element)) {
return
}
// aim to scroll just into view, so that the bottom of the element
// is just above the bottom of the container
let scrollTopGoal = this._aboveBottom(element)
// can't have a negative scroll, so put it to the top
if (scrollTopGoal < 0) {
scrollTopGoal = 0
}
this._userScroll = false
this._container.scrollTop = scrollTopGoal
}
_isFullyVisible (element: HTMLElement) {
if (!this._container) return false
return element.offsetTop - this._container.scrollTop > 0
&& this._container.scrollTop > this._aboveBottom(element)
}
_aboveBottom (element: HTMLElement) {
// add padding, since commands expanding and collapsing can mess with
// the offset, causing the running command to be half cut off
// https://github.com/cypress-io/cypress/issues/228
const containerHeight = this._container ? this._container.clientHeight : 0
return element.offsetTop + element.clientHeight - containerHeight + PADDING
}
getScrollTop () {
return this._container ? this._container.scrollTop : 0
}
setScrollTop (scrollTop?: number | null) {
if (this._container && scrollTop != null) {
this._container.scrollTop = scrollTop
}
}
scrollToEnd () {
if (!this._container) return
this.setScrollTop(this._container.scrollHeight - this._container.clientHeight)
}
// for testing purposes
__reset () {
this._container = null
this._userScroll = true
this._userScrollCount = 0
clearTimeout(this._countUserScrollsTimeout as TimeoutID)
this._countUserScrollsTimeout = undefined
}
}
export default new Scroller()