Skip to content

πŸͺ‚ Cypress command to wait for any CSS/DOM property to be idle after n frames.

License

Notifications You must be signed in to change notification settings

smastrom/cypress-wait-frames

Repository files navigation

npm GitHub Workflow Status npm bundle size dependency-count

Cypress Wait Frames

Cypress command to wait for any CSS/DOM property to be idle after a specified number of frames.


Installation

pnpm add cypress-wait-frames

Import the package in your commands.js:

import 'cypress-wait-frames'

If using TypeScript, install the latest version of csstype for CSS properties autocompletion:

pnpm add -D csstype

Do I need this?

Cypress retry ability on built-in assertions is very powerful and most likely you don't need this package or to use cy.wait. For example:

cy.scrollTo(0, 1200)

// No need for cy.wait(t) to make sure scroll is completed

cy.window().eq('scrollY').should('be.approximately', 1200, 2) // Will retry until it passes

Documentation on retry-ability is very detailed and it might already contain the answer you're looking for.


When to use it

There are cases where it's impossible to retain retry-ability and you might find yourself guessing timings using cy.wait.

For example when asserting properties not available within Cypress queries:

cy.get('h1').eq(15).scrollIntoView()

// Need to add cy.wait(t) to make sure scroll is completed

cy.get('h1')
  .eq(15)
  .then((el) => {
    cy.wrap(el[0].getBoundingClientRect().top).should('be.approximately', 0, 2)
  })

Or when futher assertions must rely on 100% accurate values:

cy.get('#TriggerComplexTransition').click()

// Need to add cy.wait(t) to make sure desktopHeight is accurate

cy.viewport('macbook-15').get('#TransitionedElement').invoke('height').as('desktopHeight')

cy.get('@desktopHeight').then((desktopHeight) => {
  cy.viewport('ipad-2')
    .get('#TransitionedElement')
    .invoke('height')
    .should('be.equal', desktopHeight / 2)
})

But scenarios can be disparate and more complex. Bottom line is that if you find yourself using cy.wait() as last resort, this package might be for you.


Usage

Instead of guessing timings using cy.wait you can use cy.waitFrames to wait for a one or more properties to be idle after a specified number of frames.

Specify a subject to watch, one or more property and a number of frames. Command will resolve once queried properties haven't changed for that number of frames.

cy.get('h1').eq(15).scrollIntoView()

cy.waitFrames({
  subject: () => cy.get('h1').eq(15),
  property: 'getBoundingClientRect.top',
  frames: 20 // Wait for the property to be idle for 20 frames
}).then(([{ value }]) => {
  cy.wrap(value).should('be.approximately', 0, 2) // Passes in any environment
})

You can also use it to just to wait for a property to be idle:

Cypress.Commands.add('waitForResize', () => {
  cy.waitFrames({
    subject: cy.window,
    property: 'outerWidth',
    frames: 20
  })
})
cy.waitForResize()

cy.log('Resized!') // This is executed once outerWidth isn't changed for 20 frames.

Options

Property Default Type Description Required
subject undefined () => Chainable<T> Chainable to watch for properties. βœ…
property undefined string | string[] One or more properties to watch. βœ…
frames 20 number Number of frames to wait. ❌
timeout 30 * 1000 number Timeout in milliseconds. ❌

Yields

A Cypress Promise which resolves to an array of objects (one for each property) or throws an error if timeout is reached:

Property Type Description
subject AUTWindow | Document | HTMLElement | JQuery<HTMLElement> Subject yielded from subject option chainer.
value string | number | null | undefined Property value at which the function resolved.
property string Awaited property name.
time DOMHighResTimestamp Time in ms that took to resolve since invoking.

Subjects

Window

cy.waitFrames({
  subject: cy.window
})

πŸ’‘ Use cy.window to watch for window-only DOM properties like scrollY or outerWidth.

DocumentElement / HTML

cy.waitFrames({
  subject: cy.document
})

πŸ’‘ Use cy.document to watch for DOM/CSS properties on the documentElement such as clientWidth, pointer-events, overflow etc.

HTMLElement / SVGElement

cy.waitFrames({
  subject: () => cy.get('a').eq(0) // or () => cy.get('.my-selector')
})

πŸ’‘ Use () => cy.get to watch for DOM/CSS properties on any other HTMLElement.

⚠️ When using cy.get, make sure to pass a function which returns the chainable.


Properties

DOM properties

cy.waitFrames({
  subject: cy.window,
  property: 'scrollY',
  frames: 10
})

CSS properties

cy.waitFrames({
  subject: () => cy.get('.my-element'),
  property: 'background-color', // or '--my-var'
  frames: 10
})

πŸ’‘ Use kebab-case for CSS properties. getComputedStyle is used internally to get the values.

Objects / methods properties

cy.waitFrames({
  subject: cy.window,
  property: 'visualViewport.offsetTop'
})
cy.waitFrames({
  subject: () => cy.get('.my-element'),
  property: 'getBoundingClientRect.top'
})

⚠️ Bear in mind that only methods or objects with maximum 1 property which returns a primitive are supported.

Methods with arity greater than 0 are not supported, e.g. getAttribute('href').

Multiple properties / methods

You can watch for multiple properties as well:

cy.waitFrames({
  subject: () => cy.get('.my-element'),
  property: ['background-color', 'scrollTop', 'getBoundingClientRect.top'],
  frames: 10
})

πŸ’‘ waitFrames will resolve once all properties are idle.


License

MIT