Skip to content

Commit

Permalink
feat: add scrollBehavior option (#8837)
Browse files Browse the repository at this point in the history
Co-authored-by: Ben Kucera <14625260+Bkucera@users.noreply.github.com>
  • Loading branch information
lukeapage and kuceb authored Nov 30, 2020
1 parent b9c9c6e commit 0b07e8f
Show file tree
Hide file tree
Showing 17 changed files with 493 additions and 16 deletions.
11 changes: 11 additions & 0 deletions cli/schema/cypress.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,17 @@
"default": true,
"description": "Whether to wait for elements to finish animating before executing commands"
},
"scrollBehavior": {
"enum": [
false,
"center",
"top",
"bottom",
"nearest"
],
"default": "top",
"description": "Viewport position to which an element should be scrolled prior to action commands. Setting `false` disables scrolling."
},
"projectId": {
"type": "string",
"default": null,
Expand Down
47 changes: 36 additions & 11 deletions cli/types/cypress.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2287,20 +2287,46 @@ declare namespace Cypress {
force: boolean
}

type scrollBehaviorOptions = false | 'center' | 'top' | 'bottom' | 'nearest'

/**
* Options to affect Actionability checks
* @see https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability
*/
interface ActionableOptions extends Forceable {
/**
* Whether to wait for elements to finish animating before executing commands
*
* @default true
*/
waitForAnimations: boolean
/**
* The distance in pixels an element must exceed over time to be considered animating
* @default 5
*/
animationDistanceThreshold: number
/**
* Viewport position to which an element should be scrolled prior to action commands. Setting `false` disables scrolling.
*
* @default 'top'
*/
scrollBehavior: scrollBehaviorOptions
}

interface BlurOptions extends Loggable, Forceable { }

interface CheckOptions extends Loggable, Timeoutable, Forceable {
interface CheckOptions extends Loggable, Timeoutable, ActionableOptions {
interval: number
}

interface ClearOptions extends Loggable, Timeoutable, Forceable {
interface ClearOptions extends Loggable, Timeoutable, ActionableOptions {
interval: number
}

/**
* Object to change the default behavior of .click().
*/
interface ClickOptions extends Loggable, Timeoutable, Forceable {
interface ClickOptions extends Loggable, Timeoutable, ActionableOptions {
/**
* Serially click multiple elements
*
Expand Down Expand Up @@ -2528,6 +2554,11 @@ declare namespace Cypress {
* @default true
*/
waitForAnimations: boolean
/**
* Viewport position to which an element should be scrolled prior to action commands. Setting `false` disables scrolling.
* @default 'top'
*/
scrollBehavior: scrollBehaviorOptions
/**
* Firefox version 79 and below only: The number of tests that will run between forced garbage collections.
* If a number is supplied, it will apply to `run` mode and `open` mode.
Expand Down Expand Up @@ -2754,7 +2785,7 @@ declare namespace Cypress {
*
* @see https://on.cypress.io/type
*/
interface TypeOptions extends Loggable, Timeoutable {
interface TypeOptions extends Loggable, Timeoutable, ActionableOptions {
/**
* Delay after each keypress (ms)
*
Expand All @@ -2768,12 +2799,6 @@ declare namespace Cypress {
* @default true
*/
parseSpecialCharSequences: boolean
/**
* Forces the action, disables waiting for actionability
*
* @default false
*/
force: boolean
/**
* Keep a modifier activated between commands
*
Expand Down Expand Up @@ -2866,7 +2891,7 @@ declare namespace Cypress {
/**
* Options to change the default behavior of .trigger()
*/
interface TriggerOptions extends Loggable, Timeoutable, Forceable {
interface TriggerOptions extends Loggable, Timeoutable, ActionableOptions {
/**
* Whether the event bubbles
*
Expand Down
9 changes: 9 additions & 0 deletions cli/types/tests/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,12 @@ Cypress.on('test:after:run', (attributes , test) => {
attributes // $ExpectType ObjectLike
test // $ExpectType Test
})

namespace CypressActionCommandOptionTests {
cy.get('el').clear({scrollBehavior: 'top'})
cy.get('el').check({scrollBehavior: 'bottom'})
cy.get('el').type('hello', {scrollBehavior: 'center'})
cy.get('el').trigger('mousedown', {scrollBehavior: 'nearest'})
cy.get('el').click({scrollBehavior: false})
cy.get('el').click({scrollBehavior: true}) // $ExpectError
}
5 changes: 5 additions & 0 deletions packages/desktop-gui/cypress/fixtures/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@
"from": "default",
"value": true
},
"scrollBehavior": {
"from": "default",
"value": "top"
},
"watchForFileChanges": {
"from": "default",
"value": true
Expand Down Expand Up @@ -411,6 +415,7 @@
"viewportHeight": 660,
"viewportWidth": 1000,
"waitForAnimations": true,
"scrollBehavior": "top",
"watchForFileChanges": true,
"xhrRoute": "/xhrs/",
"xhrUrl": "__cypress/xhrs/"
Expand Down
73 changes: 73 additions & 0 deletions packages/driver/cypress/integration/commands/actions/check_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,57 @@ describe('src/cy/commands/actions/check', () => {
cy.get('#checkbox-covered-in-span').check({ timeout: 1000, interval: 60 })
})

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

cy.get(':checkbox:first').check({ scrollBehavior: 'bottom' })

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

it('does not scroll when scrollBehavior is false in options', () => {
cy.get(':checkbox:first').scrollIntoView()
cy.get(':checkbox:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get(':checkbox:first').check({ scrollBehavior: false })

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

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.get(':checkbox:first').scrollIntoView()
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).not.to.be.called
})
})

it('calls scrollIntoView by default', () => {
cy.scrollTo('top')
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: 'start' })
})
})

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

Expand All @@ -200,6 +251,28 @@ describe('src/cy/commands/actions/check', () => {
})
})

it('can set options.waitForAnimations', () => {
cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!'))

cy.get(':checkbox:first').check({ waitForAnimations: false }).then(() => {
expect(cy.ensureElementIsNotAnimating).not.to.be.called
})
})

it('can set options.animationDistanceThreshold', () => {
const $btn = cy.$$(':checkbox:first')

cy.spy(cy, 'ensureElementIsNotAnimating')
cy.get(':checkbox:first').check({ animationDistanceThreshold: 1000 }).then(() => {
const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn)
const { args } = cy.ensureElementIsNotAnimating.firstCall

expect(args[1]).to.deep.eq([fromElWindow, fromElWindow])

expect(args[2]).to.eq(1000)
})
})

it('delays 50ms before resolving', () => {
cy.$$(':checkbox:first').on('change', (e) => {
cy.spy(Promise, 'delay')
Expand Down
48 changes: 48 additions & 0 deletions packages/driver/cypress/integration/commands/actions/clear_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,54 @@ describe('src/cy/commands/actions/type - #clear', () => {
})
})

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

cy.get('input:first').clear({ scrollBehavior: 'bottom' })

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

it('does not scroll when scrollBehavior is false in options', () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('input:first').clear({ scrollBehavior: false })

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).not.to.be.called
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

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

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).not.to.be.called
})
})

it('calls scrollIntoView by default', () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

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

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

// https://github.com/cypress-io/cypress/issues/5835
it('can force clear when hidden in input', () => {
const input = cy.$$('input:first')
Expand Down
71 changes: 71 additions & 0 deletions packages/driver/cypress/integration/commands/actions/click_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,77 @@ describe('src/cy/commands/actions/click', () => {
})
})

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

cy.get('input:first').click({ scrollBehavior: 'bottom' })

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

it('does not scroll when scrollBehavior is false in options', () => {
cy.get('input:first').then((el) => {
cy.spy(el[0], 'scrollIntoView')
})

cy.get('input:first').click({ scrollBehavior: false })

cy.get('input:first').then((el) => {
expect(el[0].scrollIntoView).not.to.be.called
})
})

it('does not scroll when scrollBehavior is false in config', { scrollBehavior: false }, () => {
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).not.to.be.called
})
})

it('calls scrollIntoView by default', () => {
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).to.be.calledWith({ block: 'start' })
})
})

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.click()` failed because the center of this element is hidden from view')
expect(cy.state('window').scrollY).to.equal(0)
expect(cy.state('window').scrollX).to.equal(0)

done()
})

// make sure the input is out of view
const $body = cy.$$('body')

$('<div>Long block 5</div>')
.css({
height: '500px',
border: '1px solid red',
marginTop: '10px',
width: '100%',
}).prependTo($body)

cy.get('input:first').click({ scrollBehavior: false, timeout: 200 })
})

it('can force click on hidden elements', () => {
cy.get('button:first').invoke('hide').click({ force: true })
})
Expand Down
Loading

2 comments on commit 0b07e8f

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0b07e8f Nov 30, 2020

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/6.0.2/circle-develop-0b07e8f789f7b11f087ef187c315e56b5c338dea/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 0b07e8f Nov 30, 2020

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/6.0.2/circle-develop-0b07e8f789f7b11f087ef187c315e56b5c338dea/cypress.tgz

Please sign in to comment.