I'm writing some tests for a React app using Testing Library. I want to check that some text appears, but I need to check it appears in a particular place because I know it already appears somewhere else.
The Testing Library documentation for queries says that the getByText query takes a container parameter, which I guessed lets you search within that container. I tried doing this, with the container and text parameters in the order specified in the docs:
const container = getByTestId('my-test-id');
expect(getByText(container, 'some text')).toBeTruthy();
and I get an error: matcher.test is not a function.
If I put the params the other way round:
const container = getByTestId('my-test-id');
expect(getByText('some text', container)).toBeTruthy();
I get a different error: Found multiple elements with the text: some text
Which means it's not searching inside the specified container.
I think I'm not understanding how getByText works. What am I doing wrong?
Better to use within for this sort of things:
render(<MyComponent />)
const { getByText } = within(screen.getByTestId('my-test-id'))
expect(getByText('some text')).toBeInTheDocument()
Another way to do this
import {render, screen} from '#testing-library/react';
...
render(<MyComponent />);
expect(screen.getByTestId('my-test-id')).toHaveTextContent('some text');
Note it is now recommended to use screen instead of the results of render.
(StackOverflow post the points to a KC Dobbs Article explaining why: react-testing-library - Screen vs Render queries)
This way you can be more precise, focusing in specific item:
expect(queryByTestId("helperText")?.textContent).toContain("Help me!");
Related
I would like to be able to print the results of wrapper.find('some selector')
I am mounting a react component
wrapper = mount(
<SomeReactComponent />
);
And I am able to print the whole contents of wrapper with console.log(wrapper.debug()) but I was wondering if there is some way to print specific results. I tried console.log(wrapper.find('some selector').debug()) but nothing prints.
How can I print more specific parts of the html rendered in wrapper?
Since you are using mount method I guess that you are using enzyme for rendering.
In that case, you can use simple find that has one argument selector. That selector is regular query selector.
const wrapper = mount(<MyComponent />);
console.log(wrapper.find('.my-component').at(0).html())
NOTE:
If you have more components that have my-component it will return array of elements so it is needed to have that .at(0) to select one item.
html() will print out plain html code.
API Reference:
https://enzymejs.github.io/enzyme/docs/api/ReactWrapper/find.html
This question exists but it didn't give a lot of data or real world explanation: What are Refs in React or React-Native and what is the importance of using them
Let's say i want to integrate to 3rd party library how ref is going to help me?
Some 3rd party libraries expose methods to interact with their components.
For example, in react-native-elements npm, they have shake method for Input component. You can use this method to shake Input element when user input is invalid.
Common use case is as follows:
import React from 'react';
import { Input, Button } from 'react-native-elements';
const [value, setValue] = useState('');
const input = React.createRef();
return (
<View>
<Input
ref={input}
onTextChange={(text) => setValue(text)}
/>
<Button
title={'Submit'}
onPress={() => {
if (!isValid(value)) {
input.current.shake();
}
}}
/>
</View>
);
This is react native example, but the similar goes to react projects. I hope you get the picture. Animations like shake cannot be easily handled with state, so it's better to use useRef to call component methods directly.
Let's say i want to integrate to 3rd party library how ref is going to help me?
Refs let you access the DOM directly, thus you can use vanilla js libraries using refs, for example you could use jQuery like $(ref). This simplifies and makes getting DOM nodes less error prone than using other techniques such as adding classes/ids to every element and then using selectors since these methods do not stop you from accessing nodes not created by you.
Long story short, Refs let you treat react elements as though they were vanilla js
React useRef help us to accessing dom elements before its rendering.
You can go through it
https://reactjs.org/docs/refs-and-the-dom.html
Whenever you want to use the properties of child from a parent, we refer it with a ref id, this is to ensure we are executing on the right child component. The properties can be either states, props of functions defined in the child component.
I'm creating a react app with useState and useContext for state management. So far this worked like a charm, but now I've come across a feature that needs something like an event:
Let's say there is a ContentPage which renders a lot of content pieces. The user can scroll through this and read the content.
And there's also a BookmarkPage. Clicking on a bookmark opens the ContentPage and scrolls to the corresponding piece of content.
This scrolling to content is a one-time action. Ideally, I would like to have an event listener in my ContentPage that consumes ScrollTo(item) events. But react pretty much prevents all use of events. DOM events can't be caught in the virtual dom and it's not possible to create custom synthetic events.
Also, the command "open up content piece XYZ" can come from many parts in the component tree (the example doesn't completely fit what I'm trying to implement). An event that just bubbles up the tree wouldn't solve the problem.
So I guess the react way is to somehow represent this event with the app state?
I have a workaround solution but it's hacky and has a problem (which is why I'm posting this question):
export interface MessageQueue{
messages: number[],
push:(num: number)=>void,
pop:()=>number
}
const defaultMessageQueue{
messages:[],
push: (num:number) => {throw new Error("don't use default");},
pop: () => {throw new Error("don't use default");}
}
export const MessageQueueContext = React.createContext<MessageQueue>(defaultMessageQueue);
In the component I'm providing this with:
const [messages, setmessages] = useState<number[]>([]);
//...
<MessageQueueContext.Provider value={{
messages: messages,
push:(num:number)=>{
setmessages([...messages, num]);
},
pop:()=>{
if(messages.length==0)return;
const message = messages[-1];
setmessages([...messages.slice(0, -1)]);
return message;
}
}}>
Now any component that needs to send or receive messages can use the Context.
Pushing a message works as expected. The Context changes and all components that use it re-render.
But popping a message also changes the context and also causes a re-render. This second re-render is wasted since there is no reason to do it.
Is there a clean way to implement actions/messages/events in a codebase that does state management with useState and useContext?
Since you're using routing in Ionic's router (React-Router), and you navigate between two pages, you can use the URL to pass params to the page:
Define the route to have an optional path param. Something like content-page/:section?
In the ContentPage, get the param (section) using React Router's useParams. Create a useEffect with section as the only changing dependency only. On first render (or if section changes) the scroll code would be called.
const { section } = useParams();
useEffect(() => {
// the code to jump to the section
}, [section]);
I am not sure why can't you use document.dispatchEvent(new CustomEvent()) with an associated eventListener.
Also if it's a matter of scrolling you can scrollIntoView using refs
I'm trying to reinitialize/ redraw all MDC components for form elements when their values change via javascript, but so far my attempts have fallen short. Is there an easy way to achieve this with a built in MDC method that I'm unaware of?
I created a custom way to reload the MDC components with a data-mdc-reload html attribute that fires on click but this isn't quite doing the job.
Here's a codepen showing the issue: https://codepen.io/oneezy/pen/XvMavP
click the UPDATE FORM VALUES button to add data
the VALUE output in red means the component is broke/ blue means it works
click the RESET button to reset data to initial state (this is broke too)
Javascript
// MDC Reload Component
function mdcReload(time = 1) {
var components = mdc.autoInit();
let reloadComponents = document.querySelectorAll('[data-mdc-reload]');
for (const reloadItem of reloadComponents) {
reloadItem.addEventListener("click", async () => {
setTimeout(function() {
components.forEach((c) => c.layout && c.layout());
}, time);
});
}
}
// Initialize MDC Components
mdcReload();
You can use the Foundations and Adapters provided by MDC.
I would suggest you make a MDC instance of every component and use the built in methods in mdc.COMPONENT.foundation_.adapter_.
Here is the modified pen
The issue was that you need to call floatLabel(true) to let the labels float after you set their values.
if (c.foundation_.adapter_ && c.foundation_.adapter_.floatLabel) {
c.foundation_.adapter_.floatLabel(true);
}
Furthermore I changed the select component to
// $(MDCselect).val('Option 3');
// Instantiate the MDC select component.
const mdcSelect = mdc.select.MDCSelect.attachTo(document.querySelector('.mdc-select'));
mdcSelect.foundation_.adapter_.setValue('Option 3');
Hope it helps !
I found that the easiest way to refresh the labels for the MDC components after setting a custom value via javascript is to send a Blur event to the input like this:
yourMDCInput.dispatchEvent(new Event('blur'))
This will leave the MDC component to decide which action it has to take, so it floats the label if there is a value set or resets the label if there is no value set.
It is quite annoying, but without digging into the code of the MDC foundation to find a better solution, I couldn't spot any better solution in the docs (which are incomplete and partly wrong anyways).
If you can, try using the MDC class instance to set your values - in that case, the MDC component is informed about the change and will behave as intended. When using autoInit, please note that the docs say the MDC class instance is attached to the root <div> while it is actually attached to the <label> where the data-mdc-auto-init attribute is set.
Assuming you wrap an MDCTextField in a <div id='my-text-field'>, you could do something like:
document.querySelector('#my-text-field label').MDCTextField.value = 'hello world'
and the field will update as expected.
Should an element's text contents be tested, or only the visibility thereof? I think this is a question of what is an implementation detail.
Example:
it('renders post body', async () => {
getPost.resolves(fakePost)
const { getByTestId } = render(<Post />)
await wait(() => getByTestId('post-body'))
expect(getByTestId('post-body')).toBeVisible()
// Should this next line be included?
expect(getByTestId('post-body')).toHaveTextContent(fakePost.body)
})
I feel like it is an implementation detail regarding how the body text is rendered, that I should only care that something was rendered.
For example, I next I want to store the body text as markdown and render it as HTML. To implement this, I must first change the test, because no longer will the stored text be equal to what is rendered in the DOM.
However, if only testing the visibility of a rendered element, there is no guarantee that element actually contains anything. I feel the test should be safer than that.
expect(getByTestId('post-body')).not.toBeEmpty() comes to mind in the jest-dom api, but that would pass even if the element contained only another element with no actual text contents.
Especially thanks to the guiding principals, I think it’s fair to say if you are testing your component or app the same way you would instruct a human to test it in production, then you are doing it right.
If your component is taking an API call, and formatting it into Markdown, then you should test that it is actually happening correctly. How the component is rendering (and mimicking it in your test) is an example of testing implementation details. Testing what the component renders is not.
I know it’s a fine line, but I think you should include your last line. I also think it’d be great if you could find a way to query by something other than test-id.