cypress: can't trigger 'onChange' for an input type='range' rendered by React

Current behavior:

...
return (
...
<div className="search-bar__form-range">
  <input type="range" min={10} max={900} step={10} value={500} onChange={(event)=>alert(`slider changed
  to:${event.target.value}`)}
  />
...
cy.get('.search-bar__form-range > input[type=range]').as('range').invoke('val', 700)
  .trigger('change');

Changes the slider value. Doesn’t trigger the onChange handler.

Desired behavior:

should trigger the onChange handler

Steps to reproduce:

Set up a React app with an input type=‘range’ and an onChange handler Try to trigger the onChange event from cypress.

Versions

“cypress”: “^2.1.0” “react”: “^15.4.2”

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 10
  • Comments: 31 (1 by maintainers)

Most upvoted comments

I have put together a workaround for this issue that does not require switching to the input event.

There are two parts:

  1. Set the value of the underlying DOM node (avoiding React’s override)
  2. Fire a change event on it such that React picks up the change.

That works like this:

// React overrides the DOM node's setter, so get the original, as per the linked Stack Overflow
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
describe('Range', () => {
  it('Updates the value and UI when changing a range input', () => {
    cy.get('input[type="range"]').then(($range) => {
      // get the DOM node
      const range = $range[0];
      // set the value manually
      nativeInputValueSetter.call(range, 15);
      // now dispatch the event
      range.dispatchEvent(new Event('change', { value: 15, bubbles: true }));
    });
  });
});

I’m fairly new to the cypress ecosystem, so perhaps someone can do this in a more cypress-y way, but this solves the problem.

Has anyone managed to program the workaround with Typescript? I’m getting always a Type Error - Illegal invocation for this:

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  'value'
)?.set

const changeRangeInputValue = ($range: JQuery<HTMLElement>) => (value: number) => {
  nativeInputValueSetter?.call($range[0], value)

  $range[0].dispatchEvent(new Event('change', { value, bubbles: true }))
}

@cjoecker, @JonathanAbdon this works with typescript -> #1570 (comment)

It doesn’t work for TypeScript. I received the error: Argument of type '{ value: number; bubbles: true; }' is not assignable to parameter of type 'EventInit'. Object literal may only specify known properties, and 'value' does not exist in type 'EventInit'

So I’ve solved my problem with this issue for TypeScript using the following code: In commands:

Cypress.Commands.add('setSliderValue', { prevSubject: 'element' },
    (subject, value) => {
        const element = subject[0]

        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype,
            'value'
        )?.set
        
        nativeInputValueSetter?.call(element, value)
        element.dispatchEvent(new Event('input', { bubbles: true }))
    }
)

declare namespace Cypress {
    interface Chainable {
        setSliderValue(value: number): Chainable<void>
    }
}

And use it in the test:

cy.get('input[name="sliderComponentInput"]').setSliderValue(25)

✋ Also effected by this same issue:

The input[type='range'] html element updates visually in the Cypress testing window, however .trigger('change') does not trigger the onChange callback function for the range input.

HTML

<label for="font-size">Font Size</label>
<input type="range" id="font-size" min="0" max="3" step="1" value="1">

Cypress

cy.get('#font-size')
  .invoke('val', 0)
  .trigger('change')

Workaround 🎉

I have implemented @davemyersworld’s workaround, and can confirm it works! 👍

I’ve taken his example a wee further and abstracted a higher order function to perform the input value change. Here is the Cypress test to change and test the input range value from 0 to 1 to 2 to 3:

// TODO: Remove `.then()` workaround and replace with commented steps when
// issue is resolved: https://github.com/cypress-io/cypress/issues/1570
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  'value'
).set
const changeRangeInputValue = $range => value => {
  nativeInputValueSetter.call($range[0], value)
  $range[0].dispatchEvent(new Event('change', { value, bubbles: true }))
}
describe('Font Settings', () => {
  it('User can adjust font settings', () => {
    cy.visit('/settings')
    cy.contains('Sample text.')
      .should('have.css', 'font-size')
      .should('eq', '24px')
    cy.get('#font-size')
      // .invoke('val', 0)
      // .trigger('change')
      .then(input => changeRangeInputValue(input)(0))
    cy.contains('Sample text.')
      .should('have.css', 'font-size')
      .should('eq', '18px')
    cy.get('#font-size')
      // .invoke('val', 1)
      // .trigger('change')
      .then(input => changeRangeInputValue(input)(1))
    cy.contains('Sample text.')
      .should('have.css', 'font-size')
      .should('eq', '24px')
    cy.get('#font-size')
      //   .invoke('val', 2)
      //   .trigger('change')
      .then(input => changeRangeInputValue(input)(2))
    cy.contains('Sample text.')
      .should('have.css', 'font-size')
      .should('eq', '44px')
    cy.get('#font-size')
      //   .invoke('val', 3)
      //   .trigger('change')
      .then(input => changeRangeInputValue(input)(3))
    cy.contains('Sample text.')
      .should('have.css', 'font-size')
      .should('eq', '50px')
  }
}

Looking forward to replacing workaround when a fix is available.

Thanks for triaging @jennifer-shehane

It looks like this issue has been solved, but I wanted to put this here in case anyone comes across this in google like I did today -

I was able to register a custom command in in the cypress/support/commands.js file:

Cypress.Commands.add("controlledInputChange", (input, value) => {
  const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
    window.HTMLInputElement.prototype,
    'value'
  ).set
  
  const changeInputValue = inputToChange => newValue => {
    nativeInputValueSetter.call(inputToChange[0], newValue)
    inputToChange[0].dispatchEvent(new Event('change', { newValue, bubbles: true }))
  }

  return cy.get(input).then(input => changeInputValue(input)(value))
})

This is used in the test as:

 cy.get('input[type="range"]').eq(0).then(input => {
   value = 10.toString() // or whatever number, but it needs to be a passed in as a string
   cy.controlledInputChange(input, value)
})

ok, so I’ve found sort of a workaround. it looks like the onInput event can be triggered just fine. In the case of input type=‘range’ the “input” event is triggered in the same situation as the “change” is so I was able to switch it.

Workaround would be: use onInput with .trigger('input') instead of onChange

I was also running into this issue and had to use beausmith’s workaround (adapted from Dave Myers’).

I had to make a couple further adjustments, so I’ll share them here, hoping they can save other people some time.

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  "value"
).set;

const changeRangeInputValue = ($range) => (value) => {
  return new Promise((resolve) => {
    nativeInputValueSetter.call($range[0], value);
    $range[0].dispatchEvent(new Event("input", { value, bubbles: true }));
    $range[0].dispatchEvent(new Event("change", { value, bubbles: true }));
    resolve();
  });
};

What changed:

  1. I needed to dispatch both input and change events. Our slider implementation uses both, maybe that’s the case for you as well.
  2. I needed to check the contents of another element right after changing the value of the slider. So, I have to wrap changeRangeInputValue’s body within a promise, otherwise Cypress will happily move on to the next command in queue, and check the contents of slider-dependent elements before they actually change (see the docs for cy.then for more info on this).

Hopefully I didn’t misunderstand any Cypress or DOM details - but the above solution, adapted from @beausmith 's, worked well for me, letting me test sliders on my project.

Need some help with the material-ui slider: I didn’t manage to move the slider with some value of offset (sideways like)

Current Cypress it(‘Slider test’, () => { cy.visit(‘https://material-ui.com/components/slider/’) cy.get( ‘.jss407 > .MuiGrid-container > .MuiGrid-grid-xs-true > .MuiSlider-root > .MuiSlider-thumb’, ) .focus() .trigger(‘mousedown’) .trigger(‘mousemove’, 0, 20, { which: 1 }) .trigger(‘mouseup’)

}) })

Versions:

  • Win10, Chrome 80
  • Cypress 4.2.0

image

Perhaps someone can help?

*** The issue is resolved by the code below: *** - Thx to Vinod Mathew@k.vinodmathew_gitlab ***

it.only('Slider test', () => {
    cy.viewport(1900, 1000);
    cy.visit('https://material-ui.com/components/slider/')
    cy.get('[aria-labelledby="discrete-slider"]').first()
      .trigger('mousedown', { which: 1 }, { force: true })
      .trigger('mousemove', 881, 288, { force: true })
      .trigger('mouseup')

thank you @davemyersworld! that fixed the issue for me!!

I have put together a workaround for this issue that does not require switching to the input event.

There are two parts:

  1. Set the value of the underlying DOM node (avoiding React’s override)
  2. Fire a change event on it such that React picks up the change.

That works like this:

// React overrides the DOM node's setter, so get the original, as per the linked Stack Overflow
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
describe('Range', () => {
  it('Updates the value and UI when changing a range input', () => {
    cy.get('input[type="range"]').then(($range) => {
      // get the DOM node
      const range = $range[0];
      // set the value manually
      nativeInputValueSetter.call(range, 15);
      // now dispatch the event
      range.dispatchEvent(new Event('change', { value: 15, bubbles: true }));
    });
  });
});

I’m fairly new to the cypress ecosystem, so perhaps someone can do this in a more cypress-y way, but this solves the problem.

@davemyersworld Can’t thank you enough for this solution!!

This might help anyone coming here looking for answers but this helped us:

cy.get('[type="range"]')
  .first()
  .invoke('val', 25)
  .trigger('change', { data: '25' })

@RobertoPegoraro, this worked for me, thanks!

Has anyone managed to program the workaround with Typescript?

I’m getting always a Type Error - Illegal invocation for this:

const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  window.HTMLInputElement.prototype,
  'value'
)?.set

const changeRangeInputValue = ($range: JQuery<HTMLElement>) => (value: number) => {
  nativeInputValueSetter?.call($range[0], value)

  $range[0].dispatchEvent(new Event('change', { value, bubbles: true }))
}

I’m encountering the same issue. Using .trigger('input') instead of the change event also didn’t affect anything.

I’ve been playing around with a number of combinations using click, mousedown, mousemove, mousemove to trigger an interaction on an input type='range', but nothing seems to work.

So far, I have this piece of code that sets and changes the value of the input in the DOM, and it moves the range slider (the circle knob), but the input still doesn’t recognize that there’s an interaction:

cy.get('input[type=range]').eq(0)
  .then(($el) => {
    cy.wrap($el)
      .click(200, 15)
      .invoke('val', 2)
      .trigger('change', { force: true })
      .invoke('attr', 'value', 2)
  });

I’ve tried .click(), .click('left'), and .click(200, 15) – positional clicking within the input, but Cypress doesn’t click on the input.

Does anyone know a good way of triggering an onChange event? Is this an actual bug?