How to set value of React Material UI Slider via Cypress? - reactjs

I'm using slider from here: https://material-ui.com/components/slider/
Trying to set it's value like:
cy.get("[data-cy=slider-population]")
.as("range")
.invoke("val", val)
.trigger("change");
However it's not moving. Anyone manage to get this working?

To make it a bit simpler for future viewers, #kevin's answer points you to all the right places to extract a working test, however here's everything without their demo code:
<Slider data-cy="slider-population" />
First, you need to add this command:
Cypress.Commands.add("reactComponent", {
prevSubject: "element"
}, ($el) => {
if ($el.length !== 1) {
throw new Error(`cy.component() requires element of length 1 but got ${$el.length}`);
}
// Query for key starting with __reactInternalInstance$ for React v16.x
const key = Object.keys($el.get(0)).find((key) => key.startsWith("__reactFiber$"));
const domFiber = $el.prop(key);
Cypress.log({
name: "component",
consoleProps() {
return {
component: domFiber,
};
},
});
return domFiber.return;
});
Then in your test if you want to set the minimum to 0, and the max to 15:
cy.get('[data-cy="slider-population"]')
.reactComponent()
.its("memoizedProps")
.invoke("onChange", null, [0, 15]);

There is an example in the Cypress Real World App, a payment application to demonstrate real-world usage of Cypress testing methods, patterns, and workflows which demonstrates this for it's purposes.
cy.setTransactionAmountRange is a command built to interact with the Material UI Slider.
It uses another Cypress custom command, cy.reactComponent to gain access to the component internals and allow methods like onChange to be invoked directly on the component instance.

Here's a spartan riff on the solution provided by Shannon Hochkins.
Usage:
cy.get("[data-cy=slider-population]")
.setSlider([0,15])
Command:
Cypress.Commands.add('setSlider', { prevSubject: 'element' },
(subject, value) => {
const key = Object.keys(subject.get(0))
.find((key) =>
key.startsWith("__reactFiber$"))
const fiberNode = subject.prop(key)
fiberNode.return.memoizedProps.onChange(null, value)
return subject
})
Typescript:
declare namespace Cypress {
interface Chainable {
setSlider(value: number | number[]): Chainable<void>
}
}

Related

Next.js: What is the best way to solve the warning about different server & client styles based on react-spring?

I'm working with Next.js and using a react-spring library to get an animation for a bottomsheet component. It works, however there is a warning appears:
Warning: Prop style did not match. Server: "transform:translate3d(0,Infinitypx,0)" Client: "transform:translate3d(0,652px,0)"
I've carefully investigated this warning and know that it's about incorrect rendering of the HTML element on the server and on the client side. It's clear that on the server side there is no viewport height and thus react-spring can't calculate normally the final value and Next.js registers it as an one value with Infinity and then blames on the client side when the value is calculated correctly due to available viewport height.
I'm wondering what is the best way to rid of this error?
Unfortunatelly I can't catch the react-spring calculation stage and to set a correct value.Tere is no API to do it and basically I just don't know the user's viewport height.
I've thinking about the using indexOf for the value and check if the Infinity presented and replace it for ex: by 0
however it still doesn't solve a problem as the final value will be different anyway.
Maybe someone has an idea or some link to docs etc. where I could find a solution for that?
Basically it's just a warning but I'd like to fix it anyway.
Here is the example code:
import { a, config, useSpring } from '#react-spring/web';
export function BottomSheet({propsHeight}) {
const finalHeight = propsHeight || height - 62;
const display = y.to((py) => (py < finalHeight ? 'flex' : 'none'));
const [{ y }, api] = useSpring(() => ({ y: finalHeight }));
const open = (dragEvent?: any) => {
const canceled = dragEvent?.canceled;
// when cancel is true, it means that the user passed the upwards threshold
// so need to change the spring config to create a nice wobbly effect
api.start({
y: 0,
immediate: false,
config: canceled ? config.wobbly : config.stiff,
});
};
const close = (velocity = 0) => {
api.start({
y: finalHeight,
immediate: false,
onResolve() {
if (onClose) {
onClose();
}
},
config: { ...config.stiff, velocity },
});
};
useEffect(() => {
// provide open/close actions to parent control
getActions(open, close);
}, []);
// pseudo hmtl. Removed all other markup to simplify things
return (<a.div
style={{
y, // Here is the problem of the server & client incorrect values
}}
/>)
}
I highly appreciate any help!
Kind Regards
The only one solution I've found so far for this use case it's rendering the component on client side only and, basically, it makes sense because this code is based on the browser API. I don't see any other possible solutions
To achieve it you can use dymanic imports and Next.js supports them well: here is the docs
You should disable the SSR in the import options and the component will be rendered on the client side only.
Like this:
import dynamic from 'next/dynamic';
const BottomSheetDynamic = dynamic(
() =>
import('#mylibrary/components/bottomsheet/BottomSheet').then(
//this component doesn't have default import so should do it in this way
(mod) => mod.BottomSheetexport
),
{
ssr: false,
}
);

How to use ace/ext/language_tools singleton in react-ace to avoid completers override/addition?

I am using two script editors (based on AceEditor) within one form, each uses separate completer. Currently completer is instantiated inside a component like this:
const langTools = ace.require("ace/ext/language_tools");
langTools.addCompleter(completer);
where completer is a prop.
This approach results in merged completions for every script field (within one form) as completers use a singleton inside language_tools.
To clarify completer1 should suggest "foo", completer2 should suggest "bar", but currently both completers suggest "foo", "bar"
In vanilla ace-editor it is recommended to copy editor completers to avoid using them by reference e.g.:
editor.completers = editor.completers.slice();
editor.completers.push(myCompleter);
Is there a way to achieve a similar result inside react-ace component?
It turned out to be as simple as using a ref to <AceEditor ref={ref} {...props}/> and then instead of relying on language_tools using ref.current.editor.completers in a similar fashion to vanilla ace-editor solution.
I ended with creating a hook for handling completer initialization and clean-up.
function useCompleter(completer?: CompleterInterface) {
// ref is required to access `editor` of vanilla ace-editor
const ref = useRef<ReactAce>(null);
// Init completer
useEffect(() => {
// Apply optional completer
if (completer && ref?.current) {
// prevent using editor.completers by reference, so separate completers per editor are possible
ref.current.editor.completers = ref.current.editor.completers.slice();
ref.current.editor.completers.push(completer);
}
return () => {
if (completer && ref?.current) {
ref.current.editor.completers = [];
}
};
}, [completer]);
return ref;
}

Component-testing #lexical/react editor: How to assert against HTML rendered by Cypress?

I want to component-test a controlled #lexical/react based input in Cypress. An example scenario:
Given I have a controlled input with initial state "Hello world"
When I click into the input
And I press "! key" + "Home key" + "Arrow right key" + "Arrow right key" + "n key"
Then input content is "Henlo world!"
How can I get the editor's text content, so I can assert against it?
I was hoping I could attach a newly-created editor instance to the rendered content to gain access to native lexical methods:
it('Works as intended', () => {
const Test: FC = () => {
const [state, setState] = useState('Hello world')
return <MyInput value={state} setValue={setState} />
}
mount(<Test />)
cy.get('[contenteditable="true"]').should(editorRoot => {
const editorHTML = editorRoot.get()[0]
const editorHtmlClone = editorHTML.cloneNode(true)
const editor = createEditor()
editor.setRootElement(editorHtmlClone as any)
editor.getEditorState().read(() => {
const textValue = $getRoot().getTextContent()
// Now I can access the editor's value
// but unfortunately it's been overwritten
})
})
})
But unfortunately this fails, because editor.setRootElement clears RootElement's contents.
I would really love to find a way to tap into Lexical's API, so I can test my functionality via the API without having to mirror it's exact implementation in my tests (for example defining exact expected html - it can change, doesn't mean much outside of Lexical and is a worse at describing my desired state).
Are there any options/workarounds to achieve this?
You can write the instance on the element or the window itself on your test environment. That's what we do for the TreeView (debugger)!
useEffect(() => {
const element = treeElementRef.current;
if (element !== null) {
element.__lexicalEditor = editor;
return () => {
element.__lexicalEditor = null;
};
}
}, [editor]);
That said, I would argue that your expectations fit a unit test more than an E2E test. An E2E test should validate the DOM, the HTML (via innerHTML) and selection (via getDOMSelection()).
If you want to learn more about how we do E2E testing you can check Lexical's.
ℹ️ We're planning to ship a separate testing package for users to be able to leverage all the utilities we have built on top of Playwright over the time rather than having to reimplement them on their own.

Material UI - Google Maps Autocomplete - Restrict to city and state?

I'm using the Google Maps Places Autocomplete that comes with Material UI and I'm stuck on trying restrict the results. When a user starts typing the only suggestions I want returned are City, State for them to select.
Here is a link to MUI's documentation with example:
Material UI - Google Places Autocomplete
Thanks!
It looks like the code from the library is using the AutocompleteService.getPlacePredictions. To achieve your use case you need to include the types properties with value of ['(cities)'] to your AutocompleteService.getPlacePredictions request. This will instructs the Places service to return results that match locality or administrative_area_level_3 as stated here
You can add it inside the fetch in the code sample just like below:
fetch({ input: inputValue, types: ['(cities)'] }, (results) => {
if (active) {
let newOptions = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
Here's the sample code. Make sure to use your API key for the sample to work.

What is the best way to trigger change or input event in react js from jQuery or plain JavaScript

We use Backbone + ReactJS bundle to build a client-side app.
Heavily relying on notorious valueLink we propagate values directly to the model via own wrapper that supports ReactJS interface for two way binding.
Now we faced the problem:
We have jquery.mask.js plugin which formats input value programmatically thus it doesn't fire React events. All this leads to situation when model receives unformatted values from user input and misses formatted ones from plugin.
It seems that React has plenty of event handling strategies depending on browser. Is there any common way to trigger change event for particular DOM element so that React will hear it?
For React 16 and React >=15.6
Setter .value= is not working as we wanted because React library overrides input value setter but we can call the function directly on the input as context.
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, 'react 16 value');
var ev2 = new Event('input', { bubbles: true});
input.dispatchEvent(ev2);
For textarea element you should use prototype of HTMLTextAreaElement class.
New codepen example.
All credits to this contributor and his solution
Outdated answer only for React <=15.5
With react-dom ^15.6.0 you can use simulated flag on the event object for the event to pass through
var ev = new Event('input', { bubbles: true});
ev.simulated = true;
element.value = 'Something new';
element.dispatchEvent(ev);
I made a codepen with an example
To understand why new flag is needed I found this comment very helpful:
The input logic in React now dedupe's change events so they don't fire
more than once per value. It listens for both browser onChange/onInput
events as well as sets on the DOM node value prop (when you update the
value via javascript). This has the side effect of meaning that if you
update the input's value manually input.value = 'foo' then dispatch a
ChangeEvent with { target: input } React will register both the set
and the event, see it's value is still `'foo', consider it a duplicate
event and swallow it.
This works fine in normal cases because a "real" browser initiated
event doesn't trigger sets on the element.value. You can bail out of
this logic secretly by tagging the event you trigger with a simulated
flag and react will always fire the event.
https://github.com/jquense/react/blob/9a93af4411a8e880bbc05392ccf2b195c97502d1/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js#L128
At least on text inputs, it appears that onChange is listening for input events:
var event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
Expanding on the answer from Grin/Dan Abramov, this works across multiple input types. Tested in React >= 15.5
const inputTypes = [
window.HTMLInputElement,
window.HTMLSelectElement,
window.HTMLTextAreaElement,
];
export const triggerInputChange = (node, value = '') => {
// only process the change on elements we know have a value setter in their constructor
if ( inputTypes.indexOf(node.__proto__.constructor) >-1 ) {
const setValue = Object.getOwnPropertyDescriptor(node.__proto__, 'value').set;
const event = new Event('input', { bubbles: true });
setValue.call(node, value);
node.dispatchEvent(event);
}
};
I know this answer comes a little late but I recently faced a similar problem. I wanted to trigger an event on a nested component. I had a list with radio and check box type widgets (they were divs that behaved like checkboxes and/or radio buttons) and in some other place in the application, if someone closed a toolbox, I needed to uncheck one.
I found a pretty simple solution, not sure if this is best practice but it works.
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': false
});
var node = document.getElementById('nodeMyComponentsEventIsConnectedTo');
node.dispatchEvent(event);
This triggered the click event on the domNode and my handler attached via react was indeed called so it behaves like I would expect if someone clicked on the element. I have not tested onChange but it should work, and not sure how this will fair in really old versions of IE but I believe the MouseEvent is supported in at least IE9 and up.
I eventually moved away from this for my particular use case because my component was very small (only a part of my application used react since i'm still learning it) and I could achieve the same thing another way without getting references to dom nodes.
UPDATE:
As others have stated in the comments, it is better to use this.refs.refname to get a reference to a dom node. In this case, refname is the ref you attached to your component via <MyComponent ref='refname' />.
You can simulate events using ReactTestUtils but that's designed for unit testing.
I'd recommend not using valueLink for this case and simply listening to change events fired by the plugin and updating the input's state in response. The two-way binding utils more as a demo than anything else; they're included in addons only to emphasize the fact that pure two-way binding isn't appropriate for most applications and that you usually need more application logic to describe the interactions in your app.
For HTMLSelectElement, i.e. <select>
var element = document.getElementById("element-id");
var trigger = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
"value"
).set;
trigger.call(element, 4); // 4 is the select option's value we want to set
var event = new Event("change", { bubbles: true });
element.dispatchEvent(event);
I stumbled upon the same issue today. While there is default support for the 'click', 'focus', 'blur' events out of the box in JavaScript, other useful events such as 'change', 'input' are not implemented (yet).
I came up with this generic solution and refactored the code based on the accepted answers.
export const triggerNativeEventFor = (elm, { event, ...valueObj }) => {
if (!(elm instanceof Element)) {
throw new Error(`Expected an Element but received ${elm} instead!`);
}
const [prop, value] = Object.entries(valueObj)[0] ?? [];
const desc = Object.getOwnPropertyDescriptor(elm.__proto__, prop);
desc?.set?.call(elm, value);
elm.dispatchEvent(new Event(event, { bubbles: true }));
};
How does it work?
triggerNativeEventFor(inputRef.current, { event: 'input', value: '' });
Any 2nd property you pass after the 'event' key-value pair, it will be taken into account and the rest will be ignored/discarded.
This is purposedfully written like this in order not to clutter arguments definition of the helper function.
The reason as to why not default to get descriptor for 'value' only is that for instance, if you have a native checkbox <input type="checkbox" />, than it doesn't have a value rather a 'checked' prop/attribute. Then you can pass your desired check state as follows:
triggerNativeEventFor(checkBoxRef.current, { event: 'input', checked: false });
I found this on React's Github issues: Works like a charm (v15.6.2)
Here is how I implemented to a Text input:
changeInputValue = newValue => {
const e = new Event('input', { bubbles: true })
const input = document.querySelector('input[name=' + this.props.name + ']')
console.log('input', input)
this.setNativeValue(input, newValue)
input.dispatchEvent(e)
}
setNativeValue (element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set
const prototype = Object.getPrototypeOf(element)
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
'value'
).set
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value)
} else {
valueSetter.call(element, value)
}
}
Triggering change events on arbitrary elements creates dependencies between components which are hard to reason about. It's better to stick with React's one-way data flow.
There is no simple snippet to trigger React's change event. The logic is implemented in ChangeEventPlugin.js and there are different code branches for different input types and browsers. Moreover, the implementation details vary across versions of React.
I have built react-trigger-change that does the thing, but it is intended to be used for testing, not as a production dependency:
let node;
ReactDOM.render(
<input
onChange={() => console.log('changed')}
ref={(input) => { node = input; }}
/>,
mountNode
);
reactTriggerChange(node); // 'changed' is logged
CodePen
well since we use functions to handle an onchange event, we can do it like this:
class Form extends Component {
constructor(props) {
super(props);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.state = { password: '' }
}
aForceChange() {
// something happened and a passwordChange
// needs to be triggered!!
// simple, just call the onChange handler
this.handlePasswordChange('my password');
}
handlePasswordChange(value) {
// do something
}
render() {
return (
<input type="text" value={this.state.password} onChange={changeEvent => this.handlePasswordChange(changeEvent.target.value)} />
);
}
}
The Event type input did not work for me on <select> but changing it to change works
useEffect(() => {
var event = new Event('change', { bubbles: true });
selectRef.current.dispatchEvent(event); // ref to the select control
}, [props.items]);
This ugly solution is what worked for me:
let ev = new CustomEvent('change', { bubbles: true });
Object.defineProperty(ev, 'target', {writable: false, value: inpt });
Object.defineProperty(ev, 'currentTarget', {writable: false, value: inpt });
const rHandle = Object.keys(inpt).find(k => k.startsWith("__reactEventHandlers"))
inpt[rHandle].onChange(ev);
A working solution can depend a bit on the implementation of the onChange function you're trying to trigger. Something that worked for me was to reach into the react props attached to the DOM element and call the function directly.
I created a helper function to grab the react props since they're suffixed with a hash like .__reactProps$fdb7odfwyz
It's probably not the most robust but it's good to know it's an option.
function getReactProps(el) {
const keys = Object.keys(el);
const propKey = keys.find(key => key.includes('reactProps'));
return el[propKey];
}
const el = document.querySelector('XX');
getReactProps(el).onChange({ target: { value: id } });
Since the onChange function was only using target.value I could pass a simple object to onChange to trigger my change.
This method can also help with stubborn react owned DOM elements that are listing for onMouseDown and do not respond to .click() like you'd expect.
getReactProps(el).onMouseDown(new Event('click'));
If you are using Backbone and React, I'd recommend one of the following,
Backbone.React.Component
react.backbone
They both help integrate Backbone models and collections with React views. You can use Backbone events just like you do with Backbone views. I've dabbled in both and didn't see much of a difference except one is a mixin and the other changes React.createClass to React.createBackboneClass.

Resources