Skip to content

Commit

Permalink
fix(driver): Sticky elements within a fixed container will not preven…
Browse files Browse the repository at this point in the history
…t an element from being scrolled to (#18441)

Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
  • Loading branch information
davidmunechika and chrisbreiding committed Nov 2, 2021
1 parent 26e4f92 commit 28ae1c1
Show file tree
Hide file tree
Showing 14 changed files with 262 additions and 30 deletions.
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

4 comments on commit 28ae1c1

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 28ae1c1 Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/8.7.1/circle-develop-28ae1c1000b45688afe1ca7f6092d929ae229d94/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 28ae1c1 Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppVeyor has built the win32 ia32 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/8.7.1/appveyor-develop-28ae1c1000b45688afe1ca7f6092d929ae229d94/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 28ae1c1 Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppVeyor has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/8.7.1/appveyor-develop-28ae1c1000b45688afe1ca7f6092d929ae229d94/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 28ae1c1 Nov 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/8.7.1/circle-develop-28ae1c1000b45688afe1ca7f6092d929ae229d94/cypress.tgz

Please sign in to comment.