Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

can't trigger 'onChange' for an input type='range' rendered by React #1570

Open
rluncasu opened this issue Apr 11, 2018 · 33 comments
Open

can't trigger 'onChange' for an input type='range' rendered by React #1570

rluncasu opened this issue Apr 11, 2018 · 33 comments
Labels
pkg/driver This is due to an issue in the packages/driver directory prevent-stale mark an issue so it is ignored by stale[bot] type: bug

Comments

@rluncasu
Copy link

rluncasu commented Apr 11, 2018

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"

@jennifer-shehane
Copy link
Member

Can confirm, onChange is not triggered in the example above within a react project - not sure if it is specifically a React issue though.

@jennifer-shehane jennifer-shehane added type: bug pkg/driver This is due to an issue in the packages/driver directory stage: needs investigating Someone from Cypress needs to look at this labels Apr 11, 2018
@rluncasu
Copy link
Author

rluncasu commented Apr 12, 2018

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

@excitement-engineer
Copy link

I am also running into the same issue.

@annezhou920
Copy link

annezhou920 commented Dec 13, 2018

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?

@sbasinger
Copy link

sbasinger commented Dec 21, 2018

Same issue here. Holler if anyone knows any workarounds.

Code:

handleSlider = event => {
  this.setState({
    value: event.target.value,
  });
};

<StyledSliderValue>
  <SliderValue data-test="weeds-slider-value">{this.state.value} per acre</SliderValue>
</StyledSliderValue>
<SliderInput
  onChange={this.handleSlider}
  type="range"
  min="0"
  max="1000"
  step="100"
  value={this.state.value}
  data-test="weeds-slider"
/>

Cypress test:

it('updates value on slider', ()=> {
  cy.get('[data-test="weeds-slider"]')
    .invoke('val', 500)
    .trigger('input')
    .click()

  cy.get('[data-test="weeds-slider-value"]')
    .should('have.text', '500 per acre')
})

In Cypress, I can see the slider knob move to the center, but the value does not change. In my app, everything works fine. I've tried .trigger('change') and .trigger('input') and several different .click(), but no luck.

image

@davemyersworld
Copy link

davemyersworld commented Jan 2, 2019

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.

@annezhou920
Copy link

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

@jennifer-shehane jennifer-shehane added stage: ready for work The issue is reproducible and in scope difficulty: 2️⃣ and removed stage: needs investigating Someone from Cypress needs to look at this labels Jan 14, 2019
@Elshaikh
Copy link

Elshaikh commented Mar 10, 2019

@jennifer-shehane @annezhou920 @davemyersworld
Need your help as now i can interact with price slider , but price filtration not impacted
HTML

<div class="horizontal-slider" style="position: relative;">
    <div class="bar bar-0" style="position: absolute; left: 0px; right: 225px; will-change: left, right;"></div>
    <div class="bar bar-1" style="position: absolute; left: 0px; right: 0.0139758px; will-change: left, right;"></div>
    <div class="bar bar-2" style="position: absolute; left: 224.986px; right: 0px; will-change: left, right;"></div>
    <div class="handle handle-0 " tabindex="0" role="slider" aria-valuenow="162.08" aria-valuemin="162.08"
         aria-valuemax="9177.64" style="position: absolute; z-index: 1; left: 0px;"></div>
    <div class="handle handle-1 " tabindex="0" role="slider" aria-valuenow="9177.08" aria-valuemin="162.08"
         aria-valuemax="9177.64" style="position: absolute; z-index: 2; left: 224.986px;"></div>
</div>

cypress step

cy.get('#content-section div.horizontal-slider')
  .find('div.handle.handle-1')
  .invoke('attr', 'style', "position: absolute; z-index: 1; left: 150.025px; will-change: left;")
  .trigger('change')
cy.get('#content-section div.horizontal-slider')
  .find('div.handle.handle-0')
  .invoke('attr', 'style', "position: absolute; z-index: 1; left: 56.025px; will-change: left;")
  .click()

Screen Shot 2019-03-18 at 5 36 36 PM

@beausmith
Copy link

beausmith commented Mar 27, 2019

✋ 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

@moshie
Copy link

moshie commented Jan 23, 2020

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' })

@aquacalc
Copy link

aquacalc commented Mar 8, 2020

Same issue. I was put on track to solving it with the short article "Simulate React On-Change On Controlled Components":

https://hustle.bizongo.in/simulate-react-on-change-on-controlled-components-baa336920e04

(Which I since see is the same sol'n as in the SO reference cited above.)

@ryanfiller
Copy link

ryanfiller commented Mar 26, 2020

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)
})

@Totti10as
Copy link

Totti10as commented Mar 31, 2020

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?

@Totti10as
Copy link

Totti10as commented Mar 31, 2020

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')

@cjoecker
Copy link

cjoecker commented Apr 27, 2020

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 }))
}

@YuanGaoo
Copy link

YuanGaoo commented May 26, 2020

I'm still not able to trigger the slider. Can someone give help?

cy.get('@field') .eq(index) .find("[role='slider']") .trigger('mousedown', { which: 1 }, { force: true }) .trigger('mousemove', 32, 22, { force: true }) .trigger('mouseup')

Screen Shot 2020-05-26 at 7 23 07 PM

Screen Shot 2020-05-26 at 7 22 21 PM

@YuanGaoo
Copy link

I'm still now able to trigger the slider. Can someone give help?

cy.get('@field') .eq(index) .find("[role='slider']") .trigger('mousedown', { which: 1 }, { force: true }) .trigger('mousemove', 32, 22, { force: true }) .trigger('mouseup')

Screen Shot 2020-05-26 at 7 23 07 PM Screen Shot 2020-05-26 at 7 22 21 PM

It not moving the slider
Screen Shot 2020-05-26 at 7 25 45 PM

@neilsud
Copy link

neilsud commented Jul 17, 2020

Hi @Elshaikh are you available to solve the slider? i have similar slider type with you and cant make the slider move

image

<div data-testid="range-slider-handle" class="crang-slider-handle" tabindex="0" aria-valuemax="12345" aria-valuemin="0" aria-valuenow="0" draggable="false" role="slider" style="position: absolute; z-index: 0; cursor: grab; user-select: none; touch-action: none; transform: translate(-8px, -6px);">
  <span data-label="12" class="crang-slider-handle-label is-default" style="padding: 0px 15px;">$0</span>
</div>

@eFFeeMMe
Copy link

eFFeeMMe commented Sep 30, 2020

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.

@rub
Copy link

rub commented Sep 30, 2020

Thanks @eFFeeMMe, that works great!

@afraser
Copy link

afraser commented Dec 3, 2020

Here's a variation on @ryanfiller 's solution that is implemented as a child command for a more succinct API (I think):

Usage

cy.get('.slider-input').controlledInputChange('5000000')

Command

Cypress.Commands.add('controlledInputChange', { prevSubject: 'element'}, (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))
})

@alexp1980
Copy link

@afraser I've tried your method for a React field but doesn't work for me. I'm filling in a bunch of fields (which does work) but there's a button that activates when they are all filled it which isn't happening. I've been told this button works off the 'onChange 'event of the fields so it should work right?

Thanks

@afraser
Copy link

afraser commented Dec 10, 2020

@alexp1980 hrm... not sure what to tell you. I'm still pretty new to cypress myself and this is working for me when it comes to setting a slider input value. My other controlled inputs however don't even need it. I just use cy.get('input').type('asdf').

Maybe try adding logging to your component to verify that your values are/aren't being set. If they are, then something else is going on with your button.

@JonathanAbdon
Copy link

has anyone found the typescript version of this workaround?
@cjoecker did you end up solving the illegal invocation error??

@cjoecker
Copy link

@JonathanAbdon I couldn't solve it until now :(

@dil-sjammugani
Copy link

dil-sjammugani commented Apr 27, 2021

@davemyersworld saviour you are!! Many thanks and very much appreciated for the below:
I have been struggling to get my slider working from the last 3 days.
Your code worked like a miracle 👍

// 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 }));
    });
  });
});

@harrislee09
Copy link

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)

@RobertoPegoraro
Copy link

RobertoPegoraro commented Aug 2, 2021

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)

@Marincor
Copy link

@RobertoPegoraro, this worked for me, thanks!

@fuadbinzia
Copy link

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!!

@artalar
Copy link

artalar commented Apr 6, 2022

.type("0{backspace}") as an alternative solution

@cypress-bot cypress-bot bot added stage: icebox and removed stage: ready for work The issue is reproducible and in scope labels Apr 28, 2022
@nagash77 nagash77 added the prevent-stale mark an issue so it is ignored by stale[bot] label Apr 3, 2023
@jumaantony
Copy link

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.

This one really helped me navigate around this. My app is built using nextjs and whenever Id use cypress triggers to slide the range, it wouls reset back to zero. Thank you for this

@alexkirillovtech
Copy link

Same issue here. Holler if anyone knows any workarounds.

Code:

handleSlider = event => {
  this.setState({
    value: event.target.value,
  });
};

<StyledSliderValue>
  <SliderValue data-test="weeds-slider-value">{this.state.value} per acre</SliderValue>
</StyledSliderValue>
<SliderInput
  onChange={this.handleSlider}
  type="range"
  min="0"
  max="1000"
  step="100"
  value={this.state.value}
  data-test="weeds-slider"
/>

Cypress test:

it('updates value on slider', ()=> {
  cy.get('[data-test="weeds-slider"]')
    .invoke('val', 500)
    .trigger('input')
    .click()

  cy.get('[data-test="weeds-slider-value"]')
    .should('have.text', '500 per acre')
})

In Cypress, I can see the slider knob move to the center, but the value does not change. In my app, everything works fine. I've tried .trigger('change') and .trigger('input') and several different .click(), but no luck.

image

@sbasinger Thank you from 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg/driver This is due to an issue in the packages/driver directory prevent-stale mark an issue so it is ignored by stale[bot] type: bug
Projects
None yet
Development

No branches or pull requests