Testing library React - expect multiple elements to pass a specified condition - reactjs

I have multiple buttons in my component and all of them should be disabled.
const buttons = screen.getAllByRole('button');
expect(ALL BUTTONS).toHaveAttribute('disabled'); // I want something like this.
How can we write this in the most convenient way?

Iterate the buttons array and run the expect for each one
buttons.forEach((button) => {
expect(button).toHaveAttribute('disabled');
})

I used this code in my test:
buttons.forEach((button) =>{
expect(button).toBeDisabled()
})
or you can write this:
expect(buttons).toBeDisabled().toHaveLength(2)//here you put number of your buttons

Related

useMemo on useState with Array and many Object

My apps run although its kind of slow since I loop to create button and loop again to change the color of the button
Its slow because every time I change a color in a button I have to loop in useState array of objects
My question is, how do I only render one object in an Array that uses useState ?
Or.. is there any faster way / better way than looping like this ?
I also try to target the checked status with booleanMonth[0].checked=true which change the status without looping but it doesn't change the color.
this is the array:
const [booleanMonth,setMonth]=useState([
{key:0,value:'January',title:'Jan',color:'black',checked:false},
{key:1,value:'February',title:'Feb',color:'black',checked:false},
{key:2,value:'March',title:'Mar',color:'black',checked:false},
{key:3,value:'April',title:'Apr',color:'black',checked:false},
{key:4,value:'May',title:'May',color:'black',checked:false},
{key:5,value:'June',title:'Jun',color:'black',checked:false},
{key:6,value:'July',title:'Jul',color:'black',checked:false},
{key:7,value:'August',title:'Aug',color:'black',checked:false},
{key:8,value:'September',title:'Sep',color:'black',checked:false},
{key:9,value:'October',title:'Oct',color:'black',checked:false},
{key:10,value:'November',title:'Nov',color:'black',checked:false},
{key:11,value:'December',title:'Dec',color:'black',checked:false}
])
this is where I create buttons which will loop if one of the object in array ABOVE changed because I pressed a button :
const createButtonMonth = useMemo(() =>{
console.log('createButtonMonth')
return (<View style={styles.containerForButtons2}>
{
booleanMonth.map((item,key) =>
<View key={item.key} style={styles.buttonFilter3}>
<Button style={styles.buttonFilter3}
title={item.title}
value={item.checked}
onCheckColor='red'
color={item.checked==true ? 'green':'black'}
onPress={()=>onPressMonthFilter(booleanMonth[item.key].key,booleanMonth[item.key].checked)}
/></View>)
}
</View>)
},[booleanMonth])
this is the press button function which also loop to determine which button to change its color because its checked status true / false (I think this slows me down also because of the loop)
const onPressMonthFilter = (keyMonth,statusMonth) =>{
let newArr = [...booleanMonth]
if(newArr[keyMonth].checked==false){
newArr[keyMonth].checked=true
}else{
newArr[keyMonth].checked=false
}
setMonth(newArr)
}
open for any suggestion :< help newbie here please
The only thing I can think of to further enhance this is to wrap onPressMonthFilter with useCallback
const onPressMonthFilter = useCallback((keyMonth,statusMonth) =>{
let newArr = [...booleanMonth]
if(newArr[keyMonth].checked==false){
newArr[keyMonth].checked=true
}else{
newArr[keyMonth].checked=false
}
setMonth(newArr)
}, [setMonth, booleanMonth])
So the function will return a memoized value. Other than that the code seems to be pretty straight forward, and you don't really have any choice but to loop them to assign the UI.
looping on the array that small shouldn't slow down the app tho, unless you have thousands of data on the array.
I suggest keeping the static data static and only the application state in the state. From a record perspective I know it makes sense to have all the attributes for an entity together but it but in this case I think you can just manage the state of the buttons and infer everything else. For instance, you can infer the color of the button if you know whether it is checked or not.
You can have a list of months as an static array to generate your UI. Then keep array state to keep track of buttons that are checked. If the value in the button is present in the state then button should be checked. If the button is checked then the color should be green. The action of the button will simply add its value to the state array if missing or remove it if exists.
Please review this example showcasing the suggestions
https://codesandbox.io/s/elated-bose-0vwtw

How to test select option logic with React Testing Library

I'd like to test the selection logic of my Select component.
https://codesandbox.io/s/eager-violet-kkw0x?file=/src/App.js
I found this snippet to test a simulation.
test("simulates selection", () => {
const { getByTestId, getAllByTestId } = render(<Select />);
fireEvent.change(getAllByTestId("select"), { target: { value: 2 } });
let options = getAllByTestId("select-option");
expect(options[0].selected).toBeFalsy();
expect(options[1].selected).toBeTruthy();
expect(options[2].selected).toBeFalsy();
});
However, it fails with TypeError: Cannot read property 'map' of undefined. What's going on? Thank you.
Short answer: you need to make sure you do a conditional check to skip whenever your values are undefined. You can do so by sticking in values && just before values.map(...
Secondly, I really don't see the value of using a two dimensional array just for having an index. You can store a list of fruits in an array and iterate through with a simple map, passing in the value both as a value and as an key. If you want to go fancy with the key, you can even assign the number increment that is generated by the .map()'s index along with the value, here is the fancy version:
{values && values.map((index, value) => (
<option key={`${index}-${value}`} value={value} data-testid="select">
{value}
</option>
))}
The above would generate a key like fruit-1, anotherfruit-2, etc, however you can just go with the fruit name, whatever you do, make sure you don't just go with the index number as that is not a good practice. This is all when you have a simple array, as I said, you don't need a two dimensional one.
Moving forward, there are a lot of issues in your test and some problems in the code, so it won't work unless reimagining the whole code and test, however I give my best to try and explain some of the problems and point you to the right direction:
First line with the issue is:
fireEvent.change(getAllByTestId("select"), { target: { value: 2 } });
You want to select one element, that is the <select /> so you need to use getByTestId instead of getAllByTestId, you also got the id wrong, it is select-option.
The correct format looks like this:
fireEvent.change(getByTestId("select-option"), { target: { value: 2 } });
Just a quick note, while the above works, there are other, better ways of doing this, I recommend looking into the user-event library instead of fireEvent, but most importantly, use a getByRole instead of getByTestId, read why
Next problem is that you haven't passed in your props to your select component, therefore there are no <option> elements when rendered. You couldn't do this mistake with something like TypeScript as it would warn you, but JavaScript doesn't, so.. need to pass in the props:
const props = {
callback: jest.fn(),
values: [
"grapefruit", "coconut", "lime", "mango"
],
selected: 'lime'
}
const { getByTestId, getAllByTestId } = render(<Select {...props} />);
Moving forward, when collecting options, you used the wrong ID again (I think you mixed up the two) and I would also recommend using queryAll rather than getAll, this is because queryAll would return an empty array while getAll would throw an error.
const options = queryAllByTestId("select");
Finally your assertions are all wrong as well. Your option won't have a selected attribute what then you can boolean evaluate. Your option has two attributes data-testid and value.
I tried to give you the best answer to understand what is going on, but as the code stands, it's impossible to fix it without rethinking the whole app and test, like I mentioned above so here is my advice:
In the components:
Change to two dimensional array and use the value as the index.
You are not doing anything with the selected value, just console logging out, if you pass it back to your parent component, save it into the parent state and do whatever you want with it, Probably display somewhere - this is actually important for the test.
selected prop has a string, that will probably need to have the value that you pass back and save it to the state in the parent.
In the test:
React testing library is great because your tests resemble to how the users would use the app, therefore you should assert if the selected component appears where you want, instead of trying to inspect attributes, that would work the following way:
Make sure you render the component that will have both the select component and the component you will render the selected value to as text.
Make sure you pass in all props to the component you rendering with rtl
Simulate action (recommend the user-event library, but fireEvent will also work)
use the getByText to see if the value rendered and exists on the page. That would be something like: expect(getByText('fruitName')).toBeInTheDocument();
Finally, I recommend looking into the selector precedence (use getByRole where you can) and the difference between getBy and queryBy.

Is it possible to save components state when they are stored in an array manipulated?

I'm trying to create a stepper form
I store my steps in an array of json with a proprety component ({typeOfComponent, component, key})
It works wells, but:
Everytime i slice my array, like when i move up/down a step or add a new step between two steps.
I lose the states inside my component.
I tried to use memo, i don't understand why it's only when an item position my composent is recreate. Is it possible like a pointer in C to store only his "adress"
the code sandbox exemple =>
https://codesandbox.io/s/infallible-maxwell-zkwbm?file=/src/App.js
In my real projet, the button ADD is a button for chosing the new step type
Is there any solution for manipulates my steps without losing the user data inside ?
Thanks for your help
React is re-mounting the components inside of this every re-render probably due to a variety of reasons. I couldn't get it to work as is, but by lifting the state up from your components, it will work.
You'd likely need to lift the state up anyway because the data isn't where you need it to be to make any use of your form when the user is done with it.
In order to lift the state up, I added the current value to the steps array:
function addNext(step, index) {
componentKey++;
setSteps(prevState => {
let newState = [...prevState];
step = 1;
newState.splice(index + 1, 0, {
stepNumber: step,
component: getStepContent(step, componentKey),
value: getDefaultValue(step),
key: componentKey
});
return newState;
});
}
I also made sure your getStepContent just returned the component rather than a node so you can render it like this:
<step.component
value={step.value}
onChange={handleChange}
data-index={i}
/>
There are definitely a lot of ways to optimize this if you start running into performance issues, of course.
https://codesandbox.io/s/beautiful-river-2jltr?file=/src/App.js

Can RegEx be used with React Testing Library to check classnames?

Here's a line from one of my React Testing Library tests:
expect(queryByTestId('tile-sample-service')).toHaveClass('regularTile-0-2-24', 'btn', 'btn-outline-secondary');
While it works, the test is fragile because every time the structure of the component changes, I need to go back and fix the numbers, which have changed.
Is there a way to use toHaveClass with RegEx queries or is there some other way to check if classes are present but avoid having to add things like "0-2-24" ?
Yeah for some CSS in JS generated class names sometimes the number suffix changes.
Something like this should work:
const currentLinkAnchorEl = getByText(container, 'My Currently Active Link').closest('a');
expect(currentLinkAnchorEl.className).toMatch(/mySelectedActiveClassName/)
I think it's not possible with toHaveClass(...classNames: string[]),but you can use Shallow Renderer,try this one
import ShallowRenderer from 'react-test-renderer/shallow';
it('match claas name', () => {
const renderer = new ShallowRenderer();
renderer.render(<Component />);
expect(renderer.getRenderOutput().props.className).toMatch(/button/i);
})

Why "selected" not rendered here?

I am trying to understand how React works with the new Hooks implementation. In this example, I want the browsers to render selected items as I click on the rendered options. But as you can see, it doesn't work.
Here is the example: https://codesandbox.io/s/pjorxzyrx7
Do I have to use the useEffect in this case? Also, as I understand, useEffect couldn't render anything and only return functions. So, what am I missing here?
Thank you!
You're currently mutating the contents of the selected array instead of replacing it. React can't detect a state change when you do this.
Try something like the following:
const handleSelected = item => {
console.log(item);
console.log(selected);
setSelected([...selected, item]);
};
When updating arrays or objects as a part of a state, always make a new copy to assign so that React can properly know when to re-render.
Also, include relevant parts of your code directly in the question in the future, instead of hiding it behind a link (although including a runnable example is great!)

Resources