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);
})
Related
I'm trying to test a Reactnode with react, vitest and the teasting libary.
But the formatting from the innerHTML wil not match. How I can aviod the formatting for testing cases?
- Expected - 3
+ Received + 1
- <div>
- content
- </div>
+ <div>content</div>
Here my test code
const content: ReactElement = (<div>content</div>);
makeSUT('title 2', content);
const contentResult = await screen.findByTestId('modal-content');
expect(contentResult).toBeInTheDocument();
expect(contentResult.innerHTML).toEqual(content);
Thank you
As you can see, using innerHTML in that manner can be flaky and fail due to something as simple as difference in newlines, which is not how you want to be testing your application(s) and is irrelevant to whether it is working properly.
Instead of comparing the innerHTML of two <div> elements, to include their semantic tags, I recommend using findByText to determine if the text exists in the document.
For example:
const contentWrapper = screen.getByTestId('custom-element');
const contentResult = screen.findByText('content');
// If the component with the test id is in the document, it rendered
expect(contentWrapper).toBeInTheDocument();
// contentResult will not be find the text if it is not there and this will fail
expect(contentResult).to.exist;
Really though, the assertions above are redundant as "content" won't be in the document if the component didn't render, and getByText will throw an error if no text is found (both in the assertion, and because of how getByText functions.
Example:
const contentResult = screen.findByText('content');
expect(contentResult).to.exist;
Why do I recommend using getByText over using a test id? Testing library (which you are using and is not specific to Vitest) documentation recommends use of test ids to query elements as being the least preferable/recommended query method.
From the testing library documentation: "The user cannot see (or hear) these, so this is only recommended for cases where you can't match by role or text or it doesn't make sense (e.g. the text is dynamic).", which is in contrast to their principles of "your test should resemble how users interact with your code (component, page, etc.) as much as possible."
To see a working example of what I have described, I created a demo on StackBlitz.
I need to test that a react component is called with opened={true} prop after an button click is fired. I am using testing-library ( testing-library/react + testing-library/jest-dom).
I mocked the Component using something like
import Component from "./path-to-file/component-name"
...
jest.mock("./path-to-file/component-name", () => {
return jest.fn().mockImplementation(() => {
return null
})
})
I first tried with:
expect(Component).toBeCalledWith(expect.objectContaining({"opened": true}))
expect(Component).toHaveBeenCalledWith(expect.objectContaining({"opened": true}))
expect(Component).toHaveBeenLastCalledWith(expect.objectContaining({"opened": true}))
but I got Error: expect(jest.fn()).toBeCalledWith(...expected).
Same went for expect.objectContaining({"opened": expect.anything()})
And even for expect(Component).toBeCalledWith(expect.anything())
And the difference is empty array:
I also tried with expect(ChartMenu.mock).toBeCalledWith(expect.anything()). I got a different error but still not working (this time the error was Error: expect(received).toBeCalledWith(...expected) + Matcher error: received value must be a mock or spy function)
Thank you in advice!
EDIT: here is a simplified version of the component I want to test:
const Component = () => {
const [chartMenuOpened, setChartMenuOpened] = useState(false)
return (
<Flex>
<EllipseIcon onClick={() => setChartMenuOpened(true)}>
+
</EllipseIcon>
<ChartMenu
opened={chartMenuOpened}
close={() => setChartMenuOpened(false)}
/>
</Flex>
)
}
Basically I want to make sure that when the + icon is clicked the menu will be opened (or called with open value). The issue is that I cannot render ChartMenu because it needs multiple props and redux state.
I was able in the end to mock useState in order to check that the setState was properly called from the icon component (in order to make sure there won't be future changes on the component that will break this using this post).
But I would still really appreciate an answer to the question: if there is any way to create a spy or something similar on a react component and check the props it was called with? Mostly because this was a rather simple example and I only have one state. But this might not always be the case. Or any good idea on how to properly test this kind if interaction would be really appeciated.
I think you are on the right track to test if the component has been called with that prop, it's probably the syntax issue in your code
I learn this trick from colleague and you can try to see if this helps fix your issue.
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({
opened: true,
}),
expect.anything()
);
While the question on how to is answered, I did some extra research on this and it seems like in React components there is a second parameter refOrContext (so basically most of the time it's an empty object, but it can also be a ref or a context)
Despite pointing out the reason for the behavior, I also wanted to highlight that it is safer to use expect.anything() as the second argument (rather than just {} which would work only in most of the cases ):
More information about React second argument here
I'm trying to refactor an app to use redux toolkit but I'm running into an infinite loop when dispatching an action to set state and I suspect its because I'm not following the conventions.
What I'm attempting to do at a high level is have a useAppHandlers hook that returns all the handlers for the entire app. Similarly, I have a useAppState hook that returns a global object with all my state. Snippet from my config file:
export const useAppState = () => {
const coingeckoApiState: AppState[typeof coingeckoApi.reducerPath] = useSelector(state => state.coingeckoApi);
const connectionState: AppState['connection'] = useSelector(state => state.connection);
const currencyState: AppState['currency'] = useSelector(state => state.currency);
return { ...coingeckoApiState, ...connectionState, ...currencyState };
};
export const useAppHandlers = () => {
const connectionHandlers = useConnectionHandlers();
const currencyHandlers = useCurrencyHandlers();
return { ...connectionHandlers, ...currencyHandlers };
};
^^Does that look problematic? I unfortunately can't share the full repo because it's private.
In all the redux examples I've come across, people import useDispatch in addition to the actions they are dispatching within each component. I really don't like how it results in so many lines of code just for imports and set up ex:
const dispatch = useDispatch() repeated ad nauseam across the repo).
This repo is a real-world example of what I'm trying to avoid:
https://github.com/Uniswap/uniswap-interface/blob/4078390a4890445d1ff0ed5196fd4cb56a44de87/src/components/NavigationTabs/index.tsx#L116
Before I give up and just follow conventions, I'd like to pinpoint if the above way I'm configuring redux is the source of the infinite loops, or if its a random bug I introduced deeper in the code.
Honestly, just don't. You will never be able to bundle-split in the future if the need for that arises when creating such a god-object. Also, adding something now means you have to touch at least one more file that is not using that something - same goes for deleting.
In addition to that, writing so "wide" selectors that select a full slice, even if your components only ever consume a part of that slice is a horrible thing for your performance - those components will always rerender if anything in that slice changes, no matter if it is important for your component.
Lastly: Just ignore your imports and let your IDE take care of it for you, probably out of the box. Your IDE can auto-import them for you. You can configure your IDE (with eslint autofix or other plugins) to automatically sort your imports alphabetically and also remove unused imports on save. I'm sure there is even a plugin that will just collapse your imports for you if you don't want to see them.
PS: as for why in react-redux you usually import useDispatch everywhere, you can read a bit on the history of that decision here: https://react-redux.js.org/api/hooks#recipe-useactions
<Abc>
<Xyz data-testid="comp-xyz" prop1={pqr}/>
</Abc>
Here if Abc is a class component and we are testing Abc using react testing library, then is there a way to test the value of prop1 ?
const {getByTestId} = render(<Abc />)
Now I grab the component Xyz using testId
getByTestId("comp-xyz")
Can I get props of this component something like this ?
getByTestId("comp-xyz").props() ?
No. The idea of the testing-library is to encourage you to test the user's interaction with the screen.
Creating tests that depend on component data, such as props and state, does not help with this type of test,
because users do not interact directly with the props, but with the elements rendered through them.
Another point is that tests that depend on implementation details are fragile tests, that is,
any change in the implementation, which does not change the behavior of the component, can fail its testing
generating a 'false-negative'.
In this documentation link they explain it better: https://testing-library.com/docs/
This might help someone:
const { fixture } = await render(AppComponent)
const componentInstance = fixture.componentInstance as AppComponent
expect(componentInstance.prop).toBe(true)
https://testing-library.com/docs/angular-testing-library/api/#fixture
Note that this approach is discouraged in the docs, but sometimes you gotta do what you gotta do.
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.