Skip to content

fix(driver): Sticky elements within a fixed container will not prevent an element from being scrolled to #18441

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
271dcbd
add test
davidmunechika Oct 6, 2021
43fb341
try other scroll behavior options
davidmunechika Oct 11, 2021
496945b
refactor scroll until visible
davidmunechika Oct 11, 2021
f3d05c6
allow overriding with scrollBehavior
davidmunechika Oct 12, 2021
c833f0b
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 12, 2021
95d2211
add comment
davidmunechika Oct 13, 2021
0195ca0
rename userSetScrollBehavior
davidmunechika Oct 13, 2021
4f254e6
refactor if else
davidmunechika Oct 13, 2021
5af31e0
add custom scroll behavior test
davidmunechika Oct 13, 2021
d57dd85
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 13, 2021
3d0cebb
add tests for other scrolling commands
davidmunechika Oct 15, 2021
5e8deda
Merge branch 'issue-4233-click-sticky-element' of https://github.com/…
davidmunechika Oct 15, 2021
5d688da
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 15, 2021
2bd2851
refactor scrollBehavior option
davidmunechika Oct 18, 2021
1f83d11
Merge branch 'issue-4233-click-sticky-element' of https://github.com/…
davidmunechika Oct 18, 2021
f0ddb9b
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 18, 2021
28834b5
refactor ensure visibility
davidmunechika Oct 27, 2021
3d8eb4c
Merge branch 'issue-4233-click-sticky-element' of https://github.com/…
davidmunechika Oct 27, 2021
2f9a14f
remove chai import
davidmunechika Oct 27, 2021
981f297
fix trigger test
davidmunechika Oct 27, 2021
7a0b1c1
remove user set scroll behavior
davidmunechika Oct 27, 2021
2e9207c
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 27, 2021
15a22b5
fix clear spec
davidmunechika Oct 27, 2021
2bccbff
fix error message
davidmunechika Oct 27, 2021
b0b348b
fix select case
davidmunechika Oct 28, 2021
8330767
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Oct 28, 2021
f2a4b1f
update tests
davidmunechika Nov 1, 2021
dbbe481
Merge branch 'issue-4233-click-sticky-element' of https://github.com/…
davidmunechika Nov 1, 2021
1de8681
add comment
davidmunechika Nov 1, 2021
1d0f1f7
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Nov 1, 2021
5aa6dc7
Merge branch 'develop' into issue-4233-click-sticky-element
davidmunechika Nov 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/driver/cypress/fixtures/sticky-header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<body>
<div class="overlay-background" style="position: fixed; width: 300px">
<div class="sidepanel" id="container" style="position: absolute; height: 300px; overflow: auto;">
<header class="sticky-header" style="position: sticky; top: 0; left: 0; height: 50px; background-color: blue;">
sticky header
</header>
<div style="height: 500px">
<p>content to scroll to</p>
<input type="text" value="input">
<input type="checkbox" id="vehicle1" name="vehicle1" value="Bike">
</div>
</div>
</div>
</body>
</html>
43 changes: 43 additions & 0 deletions packages/driver/cypress/integration/commands/actions/check_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,42 @@ describe('src/cy/commands/actions/check', () => {
})
})

it('can specify scrollBehavior bottom in config', { scrollBehavior: 'bottom' }, () => {
cy.get(':checkbox:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':checkbox:first').check()

cy.get(':checkbox:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'end' })
})
})

it('can specify scrollBehavior center in config', { scrollBehavior: 'center' }, () => {
cy.get(':checkbox:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':checkbox:first').check()

cy.get(':checkbox:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'center' })
})
})

it('can specify scrollBehavior nearest in config', { scrollBehavior: 'nearest' }, () => {
cy.get(':checkbox:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':checkbox:first').check()

cy.get(':checkbox:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'nearest' })
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.get(':checkbox:first').scrollIntoView()
cy.get(':checkbox:first').then((el) => {
Expand All @@ -230,6 +266,13 @@ describe('src/cy/commands/actions/check', () => {
})
})

// https://github.com/cypress-io/cypress/issues/4233
it('can check an element behind a sticky header', () => {
cy.viewport(400, 400)
cy.visit('./fixtures/sticky-header.html')
cy.get(':checkbox:first').check()
})

it('waits until element is no longer disabled', () => {
const chk = $(':checkbox:first').prop('disabled', true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ describe('src/cy/commands/actions/type - #clear', () => {
})
})

// https://github.com/cypress-io/cypress/issues/4233
it('can scroll to an element behind a sticky header', () => {
cy.viewport(400, 400)
cy.visit('./fixtures/sticky-header.html')
cy.get('input:first').clear()
})

// https://github.com/cypress-io/cypress/issues/5835
it('can force clear when hidden in input', () => {
const input = cy.$$('input:first')
Expand Down
57 changes: 47 additions & 10 deletions packages/driver/cypress/integration/commands/actions/click_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,13 @@ describe('src/cy/commands/actions/click', () => {
cy.get('#overflow-auto-container').contains('quux').click()
})

// https://github.com/cypress-io/cypress/issues/4233
it('can click an element behind a sticky header', () => {
cy.viewport(400, 400)
cy.visit('./fixtures/sticky-header.html')
cy.get('p').click()
})

it('does not scroll when being forced', () => {
const scrolled = []

Expand Down Expand Up @@ -1224,6 +1231,42 @@ describe('src/cy/commands/actions/click', () => {
})
})

it('can specify scrollBehavior bottom in config', { scrollBehavior: 'bottom' }, () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('input:first').click()

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).calledWith({ block: 'end' })
})
})

it('can specify scrollBehavior center in config', { scrollBehavior: 'center' }, () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('input:first').click()

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).calledWith({ block: 'center' })
})
})

it('can specify scrollBehavior nearest in config', { scrollBehavior: 'nearest' }, () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('input:first').click()

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).calledWith({ block: 'nearest' })
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
Expand Down Expand Up @@ -2155,7 +2198,8 @@ describe('src/cy/commands/actions/click', () => {
cy.on('fail', (err) => {
expect(this.logs.length).eq(2)
expect(err.message).not.to.contain('CSS property: `opacity: 0`')
expect(err.message).to.contain('`cy.click()` failed because this element is not visible')
expect(err.message).to.contain('`cy.click()` failed because this element')
expect(err.message).to.contain('is being covered by another element')

done()
})
Expand Down Expand Up @@ -2282,17 +2326,10 @@ describe('src/cy/commands/actions/click', () => {
expect(lastLog.get('snapshots')[0].name).to.eq('before')
expect(lastLog.get('snapshots')[1]).to.be.an('object')
expect(lastLog.get('snapshots')[1].name).to.eq('after')
expect(err.message).to.include('`cy.click()` failed because this element is not visible:')
expect(err.message).to.include('>button ...</button>')
expect(err.message).to.include('`<button#button-covered-in-span>` is not visible because it has CSS property: `position: fixed` and it\'s being covered')
expect(err.message).to.include('>span on...</span>')
expect(err.message).to.include('`cy.click()` failed because this element:')
expect(err.message).to.include('is being covered by another element:')
expect(err.docsUrl).to.eq('https://on.cypress.io/element-cannot-be-interacted-with')

const console = lastLog.invoke('consoleProps')

expect(console['Tried to Click']).to.be.undefined
expect(console['But its Covered By']).to.be.undefined

done()
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,42 @@ describe('src/cy/commands/actions/trigger', () => {
})
})

it('can specify scrollBehavior bottom in config', { scrollBehavior: 'bottom' }, () => {
cy.get('button:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('button:first').trigger('mouseover')

cy.get('button:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'end' })
})
})

it('can specify scrollBehavior center in config', { scrollBehavior: 'center' }, () => {
cy.get('button:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('button:first').trigger('mouseover')

cy.get('button:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'center' })
})
})

it('can specify scrollBehavior nearest in config', { scrollBehavior: 'nearest' }, () => {
cy.get('button:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('button:first').trigger('mouseover')

cy.get('button:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'nearest' })
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.scrollTo('top')
cy.get('button:first').then((el) => {
Expand All @@ -680,6 +716,13 @@ describe('src/cy/commands/actions/trigger', () => {
})
})

// https://github.com/cypress-io/cypress/issues/4233
it('can check an element behind a sticky header', () => {
cy.viewport(400, 400)
cy.visit('./fixtures/sticky-header.html')
cy.get('p').trigger('mouseover')
})

it('errors when scrollBehavior is false and element is out of view and is clicked', (done) => {
cy.scrollTo('top')

Expand Down Expand Up @@ -1046,7 +1089,8 @@ describe('src/cy/commands/actions/trigger', () => {
cy.on('fail', (err) => {
expect(this.logs.length).eq(2)
expect(err.message).not.to.contain('CSS property: `opacity: 0`')
expect(err.message).to.contain('`cy.trigger()` failed because this element is not visible')
expect(err.message).to.contain('`cy.trigger()` failed because this element')
expect(err.message).to.contain('is being covered by another element')

done()
})
Expand Down
43 changes: 43 additions & 0 deletions packages/driver/cypress/integration/commands/actions/type_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,42 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})

it('can specify scrollBehavior bottom in config', { scrollBehavior: 'bottom' }, () => {
cy.get(':text:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':text:first').type('foo')

cy.get(':text:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'end' })
})
})

it('can specify scrollBehavior center in config', { scrollBehavior: 'center' }, () => {
cy.get(':text:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':text:first').type('foo')

cy.get(':text:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'center' })
})
})

it('can specify scrollBehavior nearest in config', { scrollBehavior: 'nearest' }, () => {
cy.get(':text:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':text:first').type('foo')

cy.get(':text:first').then((el) => {
expect(el[0].scrollIntoView).to.be.calledWith({ block: 'nearest' })
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.get(':text:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
Expand All @@ -432,6 +468,13 @@ describe('src/cy/commands/actions/type - #type', () => {
})
})

// https://github.com/cypress-io/cypress/issues/4233
it('can scroll to an element behind a sticky header', () => {
cy.viewport(400, 400)
cy.visit('./fixtures/sticky-header.html')
cy.get('input:first').type('foo')
})

it('errors when scrollBehavior is false and element is out of view and is clicked', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.type()` failed because the center of this element is hidden from view')
Expand Down
10 changes: 7 additions & 3 deletions packages/driver/src/cy/actionability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,9 @@ const ensureNotAnimating = function (cy, $el, coordsHistory, animationDistanceTh
cy.ensureElementIsNotAnimating($el, coordsHistory, animationDistanceThreshold)
}

const verify = function (cy, $el, options, callbacks) {
const verify = function (cy, $el, config, options, callbacks) {
_.defaults(options, {
scrollBehavior: config('scrollBehavior'),
ensure: {
position: true,
visibility: true,
Expand Down Expand Up @@ -381,9 +382,11 @@ const verify = function (cy, $el, options, callbacks) {
}
}

// ensure its visible
if (options.ensure.visibility) {
cy.ensureVisibility($el, _log)
// ensure element is visible but do not check if hidden by ancestors
// until nudging algorithm occurs
// https://whimsical.com/actionability-J38eY9K2Y3vA6uCMWtmLVA
cy.ensureStrictVisibility($el, _log)
}

if (options.ensure.notReadonly) {
Expand Down Expand Up @@ -419,6 +422,7 @@ const verify = function (cy, $el, options, callbacks) {
// this calculation is relative from the viewport so we
// only care about fromElViewport coords
$elAtCoords = options.ensure.notCovered && ensureElIsNotCovered(cy, win, $el, coords.fromElViewport, options, _log, onScroll)
cy.ensureNotHiddenByAncestors($el, _log)
}

// pass our final object into onReady
Expand Down
7 changes: 4 additions & 3 deletions packages/driver/src/cy/commands/actions/click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export default (Commands, Cypress, cy, state, config) => {
errorOnSelect: true,
waitForAnimations: config('waitForAnimations'),
animationDistanceThreshold: config('animationDistanceThreshold'),
scrollBehavior: config('scrollBehavior'),
ctrlKey: false,
controlKey: false,
altKey: false,
Expand Down Expand Up @@ -183,13 +182,15 @@ export default (Commands, Cypress, cy, state, config) => {
// properties like `total` and `_retries` are mutated by
// $actionability.verify and retrying, but each click should
// have its own full timeout
const individualOptions = { ... options }
const individualOptions = {
...options,
}

// must use callbacks here instead of .then()
// because we're issuing the clicks synchronously
// once we establish the coordinates and the element
// passes all of the internal checks
return $actionability.verify(cy, $el, individualOptions, {
return $actionability.verify(cy, $el, config, individualOptions, {
onScroll ($el, type) {
return Cypress.action('cy:scrolled', $el, type)
},
Expand Down
3 changes: 1 addition & 2 deletions packages/driver/src/cy/commands/actions/trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export default (Commands, Cypress, cy, state, config) => {
y,
waitForAnimations: config('waitForAnimations'),
animationDistanceThreshold: config('animationDistanceThreshold'),
scrollBehavior: config('scrollBehavior'),
})

if ($dom.isWindow(options.$el)) {
Expand Down Expand Up @@ -112,7 +111,7 @@ export default (Commands, Cypress, cy, state, config) => {
return dispatch(subject, state('window'), eventName, eventOptions)
}

return $actionability.verify(cy, subject, options, {
return $actionability.verify(cy, subject, config, options, {
onScroll ($el, type) {
Cypress.action('cy:scrolled', $el, type)
},
Expand Down
Loading