I have a rich text editor input field that I wanted to wrap with a debounced component. Debounced input component looks like this:
import { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
function DebouncedInput(props) {
const [value, setValue] = useState(props.value);
const debouncedSave = useDebounce((nextValue) => props.onChange(nextValue), props.delay);
const handleChange = (nextValue) => {
setValue(nextValue);
debouncedSave(nextValue);
};
return props.renderProps({ onChange: handleChange, value });
}
export default DebouncedInput;
I am using DebouncedInput as a wrapper component for MediumEditor:
<DebouncedInput
value={task.text}
onChange={(text) => onTextChange(text)}
delay={500}
renderProps={(props) => (
<MediumEditor
{...props}
id="task"
style={{ height: '100%' }}
placeholder="Task text…"
disabled={readOnly}
key={task.id}
/>
)}
/>;
MediumEditor component does some sanitation work that I would like to test, for example stripping html tags:
class MediumEditor extends React.Component {
static props = {
id: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
disabled: PropTypes.bool,
uniqueID: PropTypes.any,
placeholder: PropTypes.string,
style: PropTypes.object,
};
onChange(text) {
this.props.onChange(stripHtml(text) === '' ? '' : fixExcelPaste(text));
}
render() {
const {
id,
value,
onChange,
disabled,
placeholder,
style,
uniqueID,
...restProps
} = this.props;
return (
<div style={{ position: 'relative', height: '100%' }} {...restProps}>
{disabled && (
<div
style={{
position: 'absolute',
width: '100%',
height: '100%',
cursor: 'not-allowed',
zIndex: 1,
}}
/>
)}
<Editor
id={id}
data-testid="medium-editor"
options={{
toolbar: {
buttons: ['bold', 'italic', 'underline', 'subscript', 'superscript'],
},
spellcheck: false,
disableEditing: disabled,
placeholder: { text: placeholder || 'Skriv inn tekst...' },
}}
onChange={(text) => this.onChange(text)}
text={value}
style={{
...style,
background: disabled ? 'transparent' : 'white',
borderColor: disabled ? 'grey' : '#FF9600',
overflowY: 'auto',
color: '#444F55',
}}
/>
</div>
);
}
}
export default MediumEditor;
And this is how I am testing this:
it('not stripping html tags if there is text', async () => {
expect(editor.instance.state.text).toEqual('Lorem ipsum ...?');
const mediumEditor = editor.findByProps({ 'data-testid': 'medium-editor' });
const newText = '<p><b>New text, Flesk</b></p>';
mediumEditor.props.onChange(newText);
// jest.runAllTimers();
expect(editor.instance.state.text).toEqual(newText);
});
When I run this test I get:
Error: expect(received).toEqual(expected) // deep equality
Expected: "<p><b>New text, Flesk</b></p>"
Received: "Lorem ipsum ...?"
I have also tried running the test with jest.runAllTimers(); before checking the result, but then I get:
Error: Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
I have also tried with:
jest.advanceTimersByTime(500);
But the test keeps failing, I get the old state of the text.
It seems like the state just doesn't change for some reason, which is weird since the component used to work and the test were green before I had them wrapped with DebounceInput component.
The parent component where I have MediumEditor has a method onTextChange that should be called from the DebounceInput component since that is the function that is being passed as the onChange prop to the DebounceInput, but in the test, I can see this method is never reached. In the browser, everything works fine, so I don't know why it is not working in the test?
onTextChange(text) {
console.log('text', text);
this.setState((state) => {
return {
task: { ...state.task, text },
isDirty: true,
};
});
}
On inspecting further I could see that the correct value is being passed in the test all the way to handleChange in DebouncedInput. So, I suspect, there are some problems with lodash.debounce in this test. I am not sure if I should mock this function or does mock come with jest?
const handleChange = (nextValue) => {
console.log(nextValue);
setValue(nextValue);
debouncedSave(nextValue);
};
This is where I suspect the problem is in the test:
const useDebounce = (callback, delay) => {
const debouncedFn = useCallback(
debounce((...args) => callback(...args), delay),
[delay] // will recreate if delay changes
);
return debouncedFn;
};
I have tried with mocking debounce like this:
import debounce from 'lodash.debounce'
jest.mock('lodash.debounce');
debounce.mockImplementation(() => jest.fn(fn => fn));
That gave me error:
TypeError: _lodash.default.mockImplementation is not a function
How should I fix this?
I'm guessing that you are using enzyme (from the props access).
In order to test some code that depends on timers in jest:
mark to jest to use fake timers with call to jest.useFakeTimers()
render your component
make your change (which will start the timers, in your case is the state change), pay attention that when you change the state from enzyme, you need to call componentWrapper.update()
advance the timers using jest.runOnlyPendingTimers()
This should work.
Few side notes regarding testing react components:
If you want to test the function of onChange, test the immediate component (in your case MediumEditor), there is no point of testing the entire wrapped component for testing the onChange functionality
Don't update the state from tests, it makes your tests highly couple to specific implementation, prove, rename the state variable name, the functionality of your component won't change, but your tests will fail, since they will try to update a state of none existing state variable.
Don't call onChange props (or any other props) from test. It makes your tests more implementation aware (=high couple with component implementation), and actually they doesn't check that your component works properly, think for example that for some reason you didn't pass the onChange prop to the input, your tests will pass (since your test is calling the onChange prop), but in real it won't work.
The best approach of component testing is to simulate actions on the component like your user will do, for example, in input component, simulate a change / input event on the component (this is what your user does in real app when he types).
Related
This is piece of my react component code:
<div style={{ width }}>
<FormField
label='Select a type'
labelPlacement='top'
className='cdsdrop'
>
{({ getButtonProps }) => (
<Dropdown
{...getButtonProps({
id: 'typeDropDown',
source: data,
onChange: this.handleInputChange,
options: data
})}
/>)}
</FormField>
</div>
Am new to jest framework. I started writing testcases for submit button and reset are disabled when dropdown value is empty, after selecting dropdown buttons should get enable.
When I use props().label am getting label but when I called children am getting error.
this is mytest component
describe('Buttons should be disabled on page load', () => {
it('submit and reset buttons are disabled when type is empty', () => {
const wrapper = shallow(<CdsNettingActions/>);
const submitButton = wrapper.find('WithStyles(Component).cdssubmit');
const resetButton = wrapper.find('WithStyles(Component).cdsreset');
const dropDown = wrapper.find('WithStyles(Component).cdsdrop');
const drop1=dropDown.props().children();
console.log('drop',drop1);
expect(submitButton.prop('disabled')).toEqual(true);
expect(resetButton.prop('disabled')).toEqual(true);
});
});
But am getting below error:
TypeError: Cannot read property 'getButtonProps' of undefined
className='cdsdrop'>
When I did the console logging the children function looks as below:
getButtonProps({
id: 'typeDropDown',
borderless: true,
buttonWidth: width,
source: data,
onChange: _this4.handleInputChange,
options: data
}))
Please help me how to read options from the dropdown.
I am using shollow strong textreact 16
So your FormField's children prop is a callback that expects an object with getButtonProps property:
{({ getButtonProps }) => (
That's why when you just do
const drop1=dropDown.props().children();
it crashes - there is no object with getButtonProps. You may pass this argument, but next you will find drop1 variable contains React object, not Enzyme's ShallowWrapper. So any checks like expect(drop1.prop('something')).toEqual(2) will fail "prop is not a function".
So you either use renderProp():
const drop1 = dropDown.renderProp('children')({
getButtonProps: () => ({
id: 'typeDropDown',
borderless: true,
buttonWidth: someWidth,
source: mockedSource,
onChange: mockedOnChange,
options: mockedOptions
})
});
Or maybe it's much easier to use mount() instead.
Obligatory "new to react" paragraph here. I have this rating component I got from material-ui and i'm trying to send the value to a database.
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Rating from '#material-ui/lab/Rating';
import Box from '#material-ui/core/Box';
const labels = {
0.5: 'Worst of the Worst',
1: 'Bad',
1.5: 'Poor',
2: 'Passable',
2.5: 'Ok',
3: 'Good',
3.5: 'Damn Good',
4: 'Great',
4.5: 'Love',
5: 'Perfection',
};
const useStyles = makeStyles({
root: {
width: 200,
display: 'flex',
alignItems: 'center',
},
});
export default function HoverRating(props) {
const [value, setValue] = React.useState(2);
const [hover, setHover] = React.useState(-1);
const classes = useStyles();
const onRatingChange = (event) => {
console.log(event.target.value)
props.reduxDispatch ({ type: "RATING_CHANGE", value: event.target.value
})
}
return (
<div className={classes.root}>
<Rating
name="hover-feedback"
value={value}
defaultValue={0}
precision={0.5}
size="large"
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
onChangeActive={(event, newHover) => {
setHover(newHover);
}}
{ onRatingChange }
/>
<br/>
{value !== null && <Box ml={2}>{labels[hover !== -1 ? hover : value]}</Box>}
</div>
);
}
It doesn't like something about my onRatingChange function. I've moved it all over the place and it's still throwing errors. I just really don't understand the issue. I'm mostly getting-
"./src/components/Rating.js
Line 54:11: Parsing error: Unexpected token, expected "..."
I've been at this for hours and I salvation.
Change your code from:
{ onRatingChange }
to:
onRatingChange={onRatingChange}
and change your file extension from .js to .jsx because you are using the JSX syntax
First, you appear the be storing the rating in two places: in your local state (with React.useState), and from the looks of your onRatingChange function, in a Redux store somewhere. It would be a good idea to pick one, and use that.
As for the direct answer to your question, your syntax is wrong. You're writing your Rating component in the following way:
<Rating
// ...
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
// ...
{ onRatingChange }
/>
The Rating component expects an onChange prop. I assume you want your onRatingChange function to be called when the rating changes. As such, you'd write:
<Rating
// ...
onChange={onRatingChange}
/>
The complication here though is that you're trying to register two different handlers for the rating change event. The bottom line is, decide on one, and then pass that as a callback function to the onChange prop.
This is my first attempt to refactor code from a class component to a functional component using React hooks. The reason we're refactoring is that the component currently uses the soon-to-be-defunct componentWillReceiveProps lifecylcle method, and we haven't been able to make the other lifecycle methods work the way we want. For background, the original component had the aforementioned cWRP lifecycle method, a handleChange function, was using connect and mapStateToProps, and is linking to a repository of tableau dashboards via the tableau API. I am also breaking the component, which had four distinct features, into their own components. The code I'm having issues with is this:
const Parameter = (props) => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
let parameterSelections = parameterCurrent;
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [props.parameterCurrent])
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
console.log(parameterCurrent[key]);
return (
prevState => ({
...prevState,
parameterSelections: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
Swal.fire({
position: "center",
icon: "success",
title:
JSON.stringify(key) + " set to " + JSON.stringify(valKey),
font: "1em",
showConfirmButton: false,
timer: 2500,
heightAuto: false,
height: "20px"
});
})
.otherwise(function (err) {
alert(
Swal.fire({
position: "top-end",
icon: "error",
title: err,
showConfirmButton: false,
timer: 1500,
width: "16rem",
height: "5rem"
})
);
});
}
);
};
const classes = useStyles();
return (
<div>
{Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
console.log(parameterSelections[key])
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterSelections[key]}
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
}
</div >
)};
export default Parameter;
The classes const is defined separately, and all imports of reducers, etc. have been completed. parameterSelect in the code points to all available parameters, while parameterCurrent points to the default parameters chosen in the dashboard (i.e. what the viz initially loads with).
Two things are happening: 1. Everything loads fine on initial vizualization, and when I click on the Radio Button to change the parameter, I can see it update on the dashboard - however, it's not actually showing the radio button as being selected (it still shows whichever parameter the viz initialized with as being selected). 2. When I click outside of the Filterbar (where this component is imported to), I get Uncaught TypeError: func.apply is not a function. I refactored another component and didn't have this issue, and I can't seem to determine if I coded incorrectly in the useEffect hook, the handleParameterChange function, or somewhere in the return statement. Any help is greatly appreciated by this newbie!!!
This is a lot of code to take in without seeing the original class or having a code sandbox to load up. My initial thought is it might be your useEffect
In your refactored code, you tell your useEffect to only re-run when the props.parameterCurrent changes. However inside the useEffect you don't make use of props.parameterCurrent, you instead make use of parameterCurrent from the local lexical scope. General rule of thumb, any values used in the calculations inside a useEffect should be in the list of re-run dependencies.
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [parameterCurrent])
However, this useEffect doesn't seem to do anything, so while its dependency list is incorrect, I don't think it'll solve the problem you are describing.
I would look at your dispatch and selector. Double check that the redux store is being updated as expected, and that the new value is making it from the change callback, to the store, and back down without being lost due to improper nesting, bad key names, etc...
I'd recommend posting a CodeSandbox.io link or the original class for further help debugging.
I have created my custom Autocomplete (Autosuggestions) component. Everything works fine when I pass a hardcoded array of string to autocomplete component, but when I try to pass data from API as a prop, nothing is showing for the first time I search. Results are showing each time exactly after the first time
I have tried different options but seems like when a user is searching for the first time data is not there and autocomplete is rendered with an empty array. I have tested same API endpoint and it's returning data as it should every time you search.
Home component which holds Autocomplete
const filteredUsers = this.props.searchUsers.map((item) => item.firstName).filter((item) => item !== null);
const autocomplete = (
<AutoComplete
items={filteredUsers}
placeholder="Search..."
label="Search"
onTextChanged={this.searchUsers}
fieldName="Search"
formName="autocomplete"
/>
);
AutoComplete component which filters inserted data and shows a list of suggestions, the problem is maybe inside of onTextChange:
export class AutoComplete extends Component {
constructor(props) {
super(props);
this.state = {
suggestions: [],
text: '',
};
}
// Matching and filtering suggestions fetched from the backend and text that user has entered
onTextChanged = (e) => {
const value = e.target.value;
let suggestions = [];
if (value.length > 0) {
this.props.onTextChanged(value);
const regex = new RegExp(`^${value}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
}
this.setState({ suggestions, text: value });
};
// Update state each time user press suggestion
suggestionSelected = (value) => {
this.setState(() => ({
text: value,
suggestions: []
}));
};
// User pressed the enter key
onPressEnter = (e) => {
if (e.keyCode === 13) {
this.props.onPressEnter(this.state.text);
}
};
render() {
const { text } = this.state;
return (
<div style={styles.autocompleteContainerStyles}>
<Field
label={this.props.placeholder}
onKeyDown={this.onPressEnter}
onFocus={this.props.onFocus}
name={this.props.fieldName}
formValue={text}
onChange={this.onTextChanged}
component={RenderAutocompleteField}
type="text"
/>
<Suggestions
suggestions={this.state.suggestions}
suggestionSelected={this.suggestionSelected}
theme="default"
/>
</div>
);
}
}
const styles = {
autocompleteContainerStyles: {
position: 'relative',
display: 'inline',
width: '100%'
}
};
AutoComplete.propTypes = {
items: PropTypes.array.isRequired,
placeholder: PropTypes.string.isRequired,
onTextChanged: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
onPressEnter: PropTypes.func.isRequired,
onFocus: PropTypes.func
};
export default reduxForm({
form: 'Autocomplete'
})(AutoComplete);
Expected results: Every time user use textinput to search, he should get results of suggestions
Actual results: First-time user use textinput to search, he doesn't get data. Only after first-time data is there
It works when it is hardcoded but not when using your API because your filtering happens in onTextChanged. When it is hardcoded your AutoComplete has a value to work with the first time onTextChanged (this.props.items.sort().filter(...) is called but with the API your items prop will be empty until you API returns - after this function is done.
In order to handle results from your API you will need do the filtering when the props change. The react docs actually cover a very similar case here (see the second example as the first is showing how using getDerivedStateFromProps is unnecessarily complicated), the important part being they use a PureComponent to avoid unnecessary re-renders and then do the filtering in the render, e.g. in your case:
render() {
// Derive your filtered suggestions from your props in render - this way when your API updates your items prop, it will re-render with the new data
const { text } = this.state;
const regex = new RegExp(`^${text}`, 'i');
suggestions = this.props.items.sort().filter((v) => regex.test(v));
...
<Suggestions
suggestions={suggestions}
...
/>
...
}
Is it better to render spinners, snackbars, etc. in separate DOM elements instead of adding them to the main application component tree? In React class components, it was really easy to get a reference to the class components methods to show/hide the spinner. With the new React Hooks function components, it's not so easy anymore. If I put the spinner in the main component tree, could I use the new "useContext" hook to show/hide the spinner?
Below is a React Hooks global spinner using Material-UI that works but is very hacky. How can this be made more elegant?
namespace Spinner {
'use strict';
export let show: any; // Show method ref.
export let hide: any; // Hide method ref.
export function Render() {
const [visible, setVisible] = React.useState(false); //Set refresh method.
function showIt() {
setVisible(true); // Show spinner.
}
function hideIt() {
setVisible(false); // Hide spinner.
}
const styles: any = createStyles({
col1Container: { display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
});
return (
<div>
{visible && <div style={styles.col1Container}>
<CircularProgress key={Util.uid()}
color='secondary'
size={30}
thickness={3.6}
/>
</div>}
<SetSpinnerRefs showRef={showIt} hideRef={hideIt} />
</div>
); // end return.
} // end function.
const mounted: boolean = true;
interface iProps {
showRef();
hideRef();
}
function SetSpinnerRefs(props: iProps) {
// ComponentDidMount.
React.useEffect(() => {
Spinner.show = props.showRef;
Spinner.hide = props.hideRef;
}, [mounted]);
return (<span />);
}
} // end module.
The problem is similar to this one, and a solution for spinners would be the same as the one for modal windows. React hooks don't change the way it works but can make it more concise.
There is supposed to be single spinner instance in component hierarchy:
const SpinnerContext = React.createContext();
const SpinnerContainer = props => {
const [visible, setVisible] = React.useState(false);
const spinner = useMemo(() => ({
show: () => setVisible(true),
hide: () => setVisible(false),
}), []);
render() {
return <>
{visible && <Spinner />}
<SpinnerContext.Provider value={spinner}>
{props.children}
</SpinnerContext.Provider>
</>;
}
}
Which is passed with a context:
const ComponentThatUsesSpinner = props => {
const spinner = useContext(SpinnerContext);
...
spinner.hide();
...
}
<SpinnerContainer>
...
<ComponentThatUsesSpinner />
...
</SpinnerContainer>
In React class components, it was really easy to get a reference to the class components methods to show/hide the spinner
You can continue to use class components. They are not going anywhere 🌹
The not so good way
It is actually poor practice in my opinion to use class methods to show and hide a spinner. Assuming your api looks like
<Spinner {ref=>this.something=ref}/>
And you use
this.something.show(); // or .hide
The better way
<Spinner shown={state.shown}/>
Now you get to change state.shown instead of storing the ref and using show / hide.
Although I think that Basarat's answer is the modern way of solving this problem, the below code is the way I ended up doing it. This way I only need one line of code to build the spinner and only one line of code to show/hide it.
<Spinner.Render /> {/* Build spinner component */}
Spinner.show(); //Show spinner.
namespace Spinner {
'use strict';
export let show: any; //Ref to showIt method.
export let hide: any; //Ref to hideIt method.
export function Render() {
const [visible, setVisible] = React.useState(false); //Set refresh method.
function showIt() {
setVisible(true); //Show spinner.
}
function hideIt() {
setVisible(false); //Hide spinner.
}
const showRef: any = React.useRef(showIt);
const hideRef: any = React.useRef(hideIt);
//Component did mount.
React.useEffect(() => {
Spinner.show = showRef.current;
Spinner.hide = hideRef.current;
}, []);
const styles: any = createStyles({
row1Container: { display: 'flex', alignItems: 'center', justifyContent: 'center' },
});
return (
<div>
{visible && <div style={styles.row1Container}>
<CircularProgress
color='secondary'
size={30}
thickness={3.6}
/>
</div>}
</div>
); //end return.
} //end function.
} //end module.