React Testing Library, Component Unit Tests - javascript

I am trying to build a test unit for my simple React Application using React Testing Library. I readed all docs and get stuck in it.
API was created by create React app. One of the feature is that user can change theme. There is setTheme hook that going to change theme "dark" and "light".
App.js
const App = () => {
const [theme, setTheme] = useState('dark');
return ( <div>
<Header theme={theme} setTheme={setTheme} />
</div>)
};
Header.js
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome'
const Header = props => {
return (
<header data-testid="header">
<h1><span className="highlight">Github Users</span></h1>
{props.theme === "dark" ?
<FontAwesomeIcon data-testid="button" icon="sun" size="2x" color="#dcba31" onClick={ () => props.setTheme('light') }/>
: <FontAwesomeIcon icon="moon" size="2x" color="#1c132d" onClick={ () => props.setTheme('dark') }/>}
</header>
);
}
export default Header;
In Header component I added arrow function that changes color of theme.
Now I am trying to write a test that's gonna test Header Component.
Expected result is that after first render Header component shall render icon "sun".
After user click on it header shall return icon "moon".
There is something that i try but it's not working as I mention.
Header.test.js
import React from 'react';
import { render, cleanup } from "#testing-library/react"
import '#testing-library/jest-dom/extend-expect';
import { act } from "react-dom/test-utils";
import Header from '../components/Header';
afterEach(cleanup);
describe("Header Component", () => {
it("first render should return a sun icon", () => {
const {getByTestId } = render(<Header />)
expect(getByTestId("header"). // What method uses here? To check if it is icon sun or moon ? )
})
it("after mouse click event should return a moon icon", () => {
const button = document.querySelector("svg"); // it is correct way to add a svg element as a button ?
act( () => {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
})
expect(getByTestId("header"). // again what to write here to test it after click ?
})
})
I am sure that there is some other way to check first render and then after click what's Header component rendering. I think that problem is that there is another Component that is rendered conditionaly. If it is text there is no problem, but after render there is svg element with some attributes like icon="sun" / icon="moon".
Live version of project
Github Repo Link
Questions:
How to properly test that Header component ?
How to pass props in test for example I want to use that setTheme hook in test how to do it ?

There's many ways to do this and I can recommend the articles here https://kentcdodds.com/blog/?q=test to get you started. As for your current set-up I'd change some stuff that I find helpful writing unit tests:
Use data-testid to find elements, e.g. "first render should return a sun icon" can be confirmed by expect(getByTestId('svg-sun')).toBeDefined(), which is a pattern I like
Structure your it's like stubbing-rendering-assertions and only test one thing in each test, for instance, in your second it you're lacking a rendering part of the test
Regarding your question regarding passing the props, you can pass it as render(<Header theme={theme} setTheme={setThemeStub}/>) where const setThemeStub = jest.fn(). This allows you to make assertions as expect(setThemeStub).toBeCalledWith(...)

Related

Finding the buttons on the screen that have no text for the test

I am trying to write the tests for the NavBar component (using react-native-testing-library) that has several buttons that are basically just icons (using ui-kitten for react native). So I can't get these buttons by text (as there is none) but other methods didn't work for me either (like adding accesibilityLabel or testID and then getting by the label text / getting by test ID). Any ideas what I am doing wrong?
// NavBar.tsx
import React from 'react';
import {View, StyleSheet} from 'react-native';
import {HomeBtn, SaveBtn} from '../components/buttons';
import UserSignOut from './UserSignOut';
const NavBar = ({
navigation,
pressHandlers,
}) => {
return (
<View style={styles.navBar}>
<View>
<HomeBtn navigation={navigation} />
<SaveBtn pressHandler={pressHandlers?.saveBtn ?? undefined} />
</View>
<UserSignOut />
</View>
);
};
export default NavBar;
// HomeBtn.tsx
import React from 'react';
import {Button} from '#ui-kitten/components';
import {HomeIcon} from '../shared/icons';
import styles from './Btn.style';
export const HomeBtn = ({navigation}: any) => {
return (
<Button
accesibilityLabel="home button"
style={styles.button}
accessoryLeft={props => HomeIcon(props, styles.icon)}
onPress={() => navigation.navigate('Home')}
/>
);
};
// NavBar.test.tsx
import React from 'react';
import {render, screen} from '#testing-library/react-native';
import * as eva from '#eva-design/eva';
import {RootSiblingParent} from 'react-native-root-siblings';
import {EvaIconsPack} from '#ui-kitten/eva-icons';
import {ApplicationProvider, IconRegistry} from '#ui-kitten/components';
import NavBar from '../../containers/NavBar';
describe('NavBar', () => {
const navBarContainer = (
<RootSiblingParent>
<IconRegistry icons={EvaIconsPack} />
<ApplicationProvider {...eva} theme={eva.light}>
<NavBar />
</ApplicationProvider>
</RootSiblingParent>
);
it('should render the buttons', async () => {
render(navBarContainer);
// this test fails (nothing is found with this accesibility label)
await screen.findByLabelText('home button');
});
});
Query predicate
The recommended solution would be to use:
getByRole('button', { name: "home button" })
As it will require both the button role, as well as check accessibilityLabel with name option.
Alternative, but slightly less expressive way would be to use:
getByLabelText('home button')
This query will only check accessibilityLabel prop, which also should work fine.
Why is query not matching
Since you're asking why the query is not working, that depends on your test setup. It seems that you should be able to use sync getBy* query and do not need to await findBy* query, as the HomeBtn should be rendered without waiting for any async action.
What might prevent that test from working could be incorrect mocking of any of the wrapping components: RootSiblingParent, ApplicationProvider, they might be "consuming" children prop without rendering it. In order to diagnose the issue you can use debug() function from RNTL to inspect the current state of rendered components. You can also run your tests on render(<NavBar />) to verify that.
Does await screen.findByA11yLabel('home button') work? It should match the accessibilityLabel prop.

How can I call React useRef conditionally in a Portal wrapper and have better code?

I am attempting to make a simple Portal wrapper that does the following.
Can render multiple portals on the same view
Can render multiple portals by targeting parent ids. This cannot be done by passing a ref into the component since the container may live anywhere.
Can render multiple portal without any parent targets
This is what I came up with this morning. The code works exactly how I want it to work. It creates portals with or without a parent target and cleans up completely. However React says not to call hooks from conditions, which you can see I have done here by calling useRef within a ternary. I have not seen any warnings or errors but I am also developing this code in a custom setup with Webpack and Typescript. I have not pushed this code to NPM to see what happens when I import the library into a project. My guess is there's going to be issues. Is there a better way to do this?
Thanks in advance.
Calling Component:
<div>
{open1 && (
<Portal parentId="panel-1">
<Panel title="Poopster" onClose={handleClose1}>
<div>Panel</div>
</Panel>
</Portal>
)}
{open2 && (
<Portal>
<Panel onClose={handleClose2}>
<div>Panel</div>
</Panel>
</Portal>
)}
<div>
Portal Component
import * as React from 'react';
import { createPortal } from 'react-dom';
export interface PortalProps {
children: React.ReactElement;
parentId?: string;
}
export const Portal = ({
children,
parentId
}: PortalProps): React.ReactElement => {
let ref = !parentId ?
React.useRef(document.createElement('div')) :
React.useRef(document.getElementById(parentId));
React.useEffect((): VoidFunction => {
return (): void => {
if (!parentId && ref.current) {
document.body.removeChild(ref.current);
}
ref = null;
};
}, []);
React.useEffect(() => {
if (!parentId && ref.current) {
document.body.appendChild(ref.current);
}
}, [ref, parentId]);
return createPortal(children, ref.current);
};
export default Portal;
You could make it like so, for short and more intuitive
let ref = React.useRef(
parentId ? document.getElementById(parentId) : document.createElement("div")
);
This will solve the hook rules error

ReactJS - Component with optional props inside React Context

In my app, there is a bottom sheet in the top level of my navigation system. As it is showed/hidden when the user interacts with the elements of other deeper screens inside the navigation system, I have had to wrap it inside a React Context. Take a look at this idea.
Now, imagine that this context is rendering my custom bottom sheet component:
function MessageOptions({ onDeleteButtonPress, onReportButtonPress, ... }) {
...
const handleOnDeleteButtonPress = () => {
...
onDeleteButtonPress?.();
}
const handleOnReportButtonPress = () => {
...
onReportButtonPress?.();
}
...
return (
<View style={globalStyles.overlayed} pointerEvents="box-none">
<OptionsBottomSheet
ref={menuRef}
snapPoints={[155, 0]}
initialSnap={1}
topItems={getTopItems()}
bottomItems={getBottomItems()}
onCloseEnd={onCloseEnd}
/>
</View>
);
}
As you can see, it is receiving two optional props, "onDeleteButtonPress" and "onReportButtonPress".
My idea is to consume the context which renders this custom bottom sheet component, in all my chat bubbles, just to open the bottom sheet when they are long pressed.
This is the context provider:
import React, { createContext, useRef } from "react";
import useStateWithCallback from "../../hooks/useStateWithCallback";
import MessageOptions from "../../components/Messaging/Options/MessageOptions";
const MessageOptionsContext = createContext(null);
export default MessageOptionsContext;
export function MessageOptionsProvider({ children }) {
...
return (
<MessageOptionsContext.Provider
value={{
open,
close,
}}
>
{children}
<MessageOptions
ref={messageOptionsRef}
message={message}
// onReportButtonPress={onReportButtonPress}
// onDeleteButtonPress={onDeleteButtonPress}
onCloseEnd={handleOnCloseEnd}
/>
</MessageOptionsContext.Provider>
);
}
When the user presses the delete button, I will be removing the specific message from the screen's messages list state.
How can I pass the methods from the parent (ChatScreen) of the consumer (Bubble) to the provider (MessageOptionsProvider)?
This is the flow of my app:
ChatScreen --> (contains the messages list (array) state)
|
|
---> BubbleList
|
|
---> Bubble (consumes the bottom sheet context)
I am doing this to avoid repeating my self in different routes of my app, but for being transparent, I am a little stuck in this use case.
Any ideas? It seems to be impossible?

Individual loading animation for each page with Next.js

I want each of my pages to have different loading animations when loading. How can i achieve this?
It is not possible to put the loading component on the page component like this:
//Page component
Page.Loader = SomeLoaderComponent
//_app.tsx
const Loader = Component.Loader || DefaultLoader;
This will not work because "Component(the page)" isnt loaded/rendered yet.
I have also tried dynamic import with next.js, so that i can import the correct page based on the url, and then get the correct loader. My initial plan was to add the Loader to the page component, as shown at the first line in the code above. That does not work because I have to give an explicit path.
const getLoader = (pagePath: string): ComponentType => {
const Loader = dynamic(() => import(pagePath));
return Page.Loader;
};
This is stated in the Next.js docs:
So the question is: How can I get a custom loader per page?
You can use Suspense and lazy to accomplish your task.
lets say you have ComponentA.js as follows
const ComponentA = () => {
return <div>Helloooo</div>
}
You can create another component WrapperA.js file
import React, { Suspense } from 'react';
const WrapperA = React.lazy(() => import('./ComponentA'));
function WrapperA() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<ComponentA />
</Suspense>
</div>
);
}
in the place of <div>Loading...</div> you can have any loader component you like. and export WrapperA to your routes or tab as needed.

How to communicate between React components which do not share a parent?

I am adding React to an already existing front end and am unsure how to communicate data between components.
I have a basic text Input component and a Span component, mounted separately. When the user types into the input, I want the text of the span to change to what is input.
Previously I would start a React project from scratch and so have the Input and Span share an App component as a parent. I'd use a prop function to lift the text state from the Input to the App and pass it down the value to the Span as a prop. But from scratch is not an option here.
I've considered:
Redux etc. As I'm introducing React piece by piece to this project and some team members have no React experience, I want to avoid using Redux or other state management libraries until very necessary, and it seems overkill for this simple case.
React Context API. This doesn't seem correct either, as my understanding was that context API should be kept for global data like "current authenticated user, theme, or preferred language" shared over many components, not just for sharing state between 2 components.
UseEffect hook. Using this hook to set the inner HTML of the Span component i.e
function Input() {
const inputProps = useInput("");
useEffect(() => {
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
})
return (
<div>
<h3>Name this page</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
Which sort of negates the whole point of using React for the Span?
I've gone with the UseEffect hook for now but haven't found any clear answers in the React docs or elsewhere online so any advice would be helpful.
Thanks.
Input.jsx
import React, { useState, useEffect } from 'react';
function useInput(defaultValue) {
const [value, setValue] = useState(defaultValue);
function onChange(e) {
setValue(e.target.value);
}
return {
value,
onChange
}
}
function Input() {
const inputProps = useInput("");
useEffect(() => {
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
})
return (
<div>
<h3>React asks what shall we name this product?</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
export default Input;
PageTitle.jsx
import React from 'react';
function PageTitle(props) {
var title = "Welcome!"
return (
<span>{props.title}</span>
)
}
;
export default PageTitle
Index.js
// Imports
const Main = () => (
<Input />
);
ReactDOM.render(
<Main />,
document.getElementById('react-app')
);
ReactDOM.render(
<PageTitle title="Welcome"/>,
document.getElementsByClassName('page-title')[0]
);
In React, data is supposed to flow in only one direction, from parent component to child component. Without getting into context/redux, this means keeping common state in a common ancestor of the components that need it and passing it down through props.
Your useEffect() idea isn't horrible as a kind of ad hoc solution, but I would not make PageTitle a react component, because setting the value imperatively from another component really breaks the react model.
I've used useEffect() to set things on elements that aren't in react, like the document title and body classes, as in the following code:
const siteVersion = /*value from somewhere else*/;
//...
useEffect(() => {
//put a class on body that identifies the site version
const $ = window.jQuery;
if(siteVersion && !$('body').hasClass(`site-version-${siteVersion}`)) {
$('body').addClass(`site-version-${siteVersion}`);
}
document.title = `Current Site: ${siteVersion}`;
}, [siteVersion]);
In your case, you can treat the span in a similar way, as something outside the scope of react.
Note that the second argument to useEffect() is a list of dependencies, so that useEffect() only runs whenever one or more changes.
Another side issue is that you need to guard against XSS (cross site scripting) attacks in code like this:
//setting innerHTML to an unencoded user value is dangerous
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;
Edit:
If you want to be even more tidy and react-y, you could pass a function to your input component that sets the PageTitle:
const setPageTitle = (newTitle) => {
//TODO: fix XSS problem
document.getElementsByClassName('page-title')[0].innerHTML = newTitle;
};
ReactDOM.render(
<Main setPageTitle={setPageTitle} />,
document.getElementById('react-app')
);
//inside Main:
function Input({setPageTitle}) {
const inputProps = useInput("");
useEffect(() => {
setPageTitle(inputProps.value);
})
return (
<div>
<h3>React asks what shall we name this product?</h3>
<input
placeholder="Type here"
{...inputProps}
/>
</div>
);
}
You can create a HOC or use useContext hook instead

Categories

Resources