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
2 people authored and flotwig committed Nov 8, 2021
1 parent 6508823 commit bbb825e
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

0 comments on commit bbb825e

Please sign in to comment.