React testing extracting assertions jest error - reactjs

I added eslint to a project and came across a "test has no assertions" error coming from jest/expect-expect. In the code below the render and click have been extracted because of repetition, how do I fix the error besides disabling it for the file?
it('triggers onAssignToClick when the assign button is clicked', () => {
testButtonClick('assign-button', onAssignToClickFn)
})
const testButtonClick = (buttonName, callback) => {
const { getByTestId } = renderComponent()
fireEvent.click(getByTestId(buttonName))
expect(callback).toHaveBeenCalled()
}
const renderComponent = (props) => {
return render(
<Context.Provider value={props.viewport}>
<Actions {...props} />
</Context.Provider>
)
}

Related

React Refs undefined inside functions and has values outside

I am having a lot of troubles working with react refs, what i want is to use a function declared in another components
the below code is what i am doing:
const Component1 = (props, ref) => {
const getText = () => {};
React.useImperativeHandle(ref, () => ({ getText }));
return <div />;
};
export default React.forwardRef(Component1);
const Component2 = (props) => {
const component1Ref = React.createRef();
const getTextFromComponent1 = () => {
console.log({ component1Ref }); //will be equal to {current:null}
};
console.log({ component1Ref }); //will be equal to {current:{getText}}
return <Component1 ref={component1Ref} />;
};
export default Component2;
It is very weird, the value inside getTextFromComponent1 was the same as outside, it suddenly broke! this happened with me many times
Anyone has a clue of the solution?
Features are breaking without any change
Thanks in advance
Hanan
It will take some time for ref to be initialized. I have shown an example to call it from a click event handler and within a useEffect hook.
Following works without any issues:
const Component1 = React.forwardRef((props, ref) => {
const textRef = React.createRef();
const getText = () => {
return textRef?.current?.value;
};
React.useImperativeHandle(ref, () => ({ getText }));
return <input ref={textRef} defaultValue="sample text" />;
});
const Component2 = (props) => {
const component1Ref = React.createRef();
React.useEffect(() => {
getTextFromComponent1();
}, []);
const getTextFromComponent1 = () => {
console.log(component1Ref.current?.getText());
};
return (
<div>
<button onClick={getTextFromComponent1}>Check text</button>
<br />
<Component1 ref={component1Ref} />
</div>
);
};
export default Component2;
Working Demo

Error Boundry not catching some errors in NextJS

I'm trying to add a ErrorBoundry component to my NextJS project called ErrorShield
ErrorShield:
import { Component, useState } from "react";
export class Catch extends Component {
componentDidCatch(error, info) {
const { component } = this.props;
this.props.setErrorInfo({
hasError: true,
error,
info,
component,
});
}
render() {
const { hasError, children, errorMessage } = this.props;
if (hasError) errorMessage();
return hasError ? null : children;
}
}
const ErrorShield = (props) => {
const { children } = props;
const [errorInfo, setErrorInfo] = useState({
component: "",
error: null,
info: null,
hasError: false,
});
const errorMessage = () => {
if (errorInfo.hasError) {
console.groupCollapsed(
`%c ⛔ Error in the component: ${errorInfo.component} `,
"color:#ff3333;",
);
console.groupCollapsed("%c 📖 Props:", "color:#039BE5;");
console.table(children.props);
console.groupEnd();
console.groupCollapsed("%c 🚨 Error:", "color:#F44336;");
console.log(errorInfo.error);
console.groupEnd();
console.groupEnd();
}
};
return (
<Catch
{...props}
hasError={errorInfo.hasError}
errorMessage={errorMessage}
setErrorInfo={setErrorInfo}
/>
);
};
export default ErrorShield;
At first it was working as intended but I notices that sometimes if I break some components the ErrorShield won`t catch the error, I tried to see if the behavior had any consistency but I cant find it.
For example, my project has a Header like this:
const Header = (props) => {
const { navigation } = props;
const [titles, setTitles] = useState(navigation);
.....
{/*HeaderLinkContainer, HeaderLinkBox, HeaderLiveLink and HeaderSubLink are styled components*/}
<HeaderLinkContainer variant="Transparent" as="nav">
<HeaderLinkBox variant="Transparent">
{titles.map((title) => {
let LinkTag;
if (title.live) {
LinkTag = HeaderLiveLink;
} else if (title.subnav) {
LinkTag = HeaderSubLink;
} else {
LinkTag = HeaderLink;
}
return (
<LinkTag
href={title.path}
key={title.name}
active={SECTION[0] === title.path}
>
{title.name}
</LinkTag>
);
})}
</HeaderLinkBox>
</HeaderLinkContainer>
And I call that component in the layout:
<ErrorShield component="layout/LayoutResponsive -> Header">
<Header
breadcrumbs={breadcrumbs}
social={social}
viewTickerAmp={viewTickerAmp}
/>
</ErrorShield>
If I break this component by changing this:
{titles.map((title) => {
into this:
{/*Added an extra t*/}
{titles.map((ttitle) => {
The ErrorShield does work as intended, it renders nothing and prints this on the console:
However if I break it like this:
{/*Added an extra t at the beginning*/}
{ttitles.map((title) => {
It does not catch the error and I get this message in the console:
I have another component named ContentCarrusel with a similar .map() in it but the ErrorShield does work in it without problems:
{/*ContentCarouselStyles, ContentCarouselTitleStyles, ContentCarouselThumbStyles are styled components */}
{/*ItemCarousel is a child component*/}
<ContentCarouselStyles>
<ContentCarouselTitleStyles variant="h3">Para Ti</ContentCarouselTitleStyles>
{items.map((item, index) => {
return (
<ContentCarouselThumbStyles key={item.key}>
<ItemCarousel {...item} isAmp={isAmp} />
</ContentCarouselThumbStyles>
);
})}
</ContentCarouselStyles>
Here it doesn't matter if I break it like this:
{iitems.map((item, index) => {
or this:
{items.map((iitem, index) => {
The ErrorShield still catches the error and works as intended.
I have a lot more of examples of inconsistent behavior if you need them, also let me know if you need more info. Thank you

Jest: Continue testing when expected error is thrown on button click

I am trying to test a React error boundary (with react-error-boundary library) by creating a button (to test that the code inside the library is displayed) and when clicked I want to verify that the error text is displayed, here are some screenshots
Here are the 2 files used:
error-fallback.tsx
import Box from "#mui/material/Box";
function ErrorFallback({ error }: { error: Error; resetErrorBoundary: () => void }) {
return (
<Box sx={{ textAlign: "center" }}>
<p>Error displaying this section</p>
<pre>{error.message}</pre>
</Box>
);
}
export default ErrorFallback;
error-fallback.test.tsx
import { render, fireEvent } from "#testing-library/react";
import { ErrorBoundary } from "react-error-boundary";
import Box from "#mui/material/Box";
import { useState } from "react";
import ErrorFallback from "./error-fallback";
const buttonText = "Throw Error";
const errorValue = "This is a test error";
const ThrowError = () => {
throw new Error(errorValue);
};
export const ErrorFallbackTest = () => {
const [error, setError] = useState(false);
const triggerError = () => setError(true);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Box sx={{ textAlign: "center" }}>
{error && <ThrowError />}
<button onClick={triggerError}>{buttonText}</button>
</Box>
</ErrorBoundary>
);
};
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
});
Attaching Jest error log. I am not sure which is the proper way to handle this, should I expect an error with Jest? How can I add it if the error is expected to be thrown on button click?
● Console
console.error
Error: Uncaught [Error: This is a test error]
Found the answer here
https://stackoverflow.com/a/54764497/9296982
https://github.com/facebook/react/issues/11098#issuecomment-523977830
it("react boundary works", async () => {
const { getByText } = render(<ErrorFallbackTest />);
const buttonTrigger = getByText(buttonText);
const spy = jest.spyOn(console, "error");
spy.mockImplementation(() => {});
fireEvent(buttonTrigger, new MouseEvent("click", { bubbles: true }));
const boundaryText = getByText("Error displaying this section");
expect(boundaryText).toBeInTheDocument();
expect(boundaryText).toBeDefined();
expect(boundaryText).toBeVisible();
spy.mockRestore();
});

I dont know how to hunt down my problem - React

Im very new to React and im having an issue Im not sure how to troubleshoot. So im setting an array on the context when a http request fails in a custom hook
Here is my hook:
const useHttp = (requestObj: any, setData: Function) =>
{
const [isLoading, setIsLoading] = useState(false);
const ctx = useContext(GlobalContext);
const sendRequest = useCallback(() =>
{
setIsLoading(true);
fetch(requestObj.url, {
method: requestObj.method ? requestObj.method: 'GET',
headers: requestObj.headers ? requestObj.headers : {},
body: requestObj.body ? JSON.stringify(requestObj.body) : null
})
.then(res => res.json())
.then(data => {
setIsLoading(false);
setData(data);
})
.catch(err =>
{
setIsLoading(false);
ctx.setErrors([
(prevErrors: string[]) =>
{
//prevErrors.push(err.message)
let newArray = prevErrors.map((error) => {return error});
newArray.push(err.message);
return newArray;
}]
);
console.log('There was an error');
});
}, []);
return {
isLoading: isLoading,
sendRequest: sendRequest
}
}
Im using .map cos the spread operator for arrays isnt working. Im looking into it but its not important for this.
When there are errors I create a modal and then render it in my jsx. My problem is that for some reason my Modal is rendering twice. The second time it has no props and that blows up my program. I dont know why its rendering again and I dont know how to attack the problem. The stack has nothing with regards to what is causing it (that I can see). If a component is rendering again would the props not be the same as originally used? I have breakpoints in the spot where the modal is called and they arent getting hit again. So can anyone offer some advice for how I go about debugging this?
const App: FC = () => {
const [errors, setErrors] = useState([]);
let modal = null
if(errors.length > 0)
{
modal = (
<Modal
heading="Warning"
content={<div>{errors}</div>}
buttonList={
[
{label: "OK", clickHandler: ()=> {}, closesModal: true},
{label: "Cancel", clickHandler: ()=> {alert("cancelled")}, closesModal: false}
]
}
isOpen={true}/>
)
}
return (
<GlobalContext.Provider value={{errors: errors, setErrors: setErrors}}>
<ProviderV3 theme={defaultTheme}>
<Toolbar></Toolbar>
<Grid
margin='25px'
columns='50% 50%'
gap='10px'
maxWidth='100vw'>
<OwnerSearch />
<NewOwnerSearch />
</Grid>
</ProviderV3>
{modal}
</GlobalContext.Provider>
);
};
import { FC, useState } from 'react';
import {
ButtonGroup, Button, DialogContainer,
Dialog, Content, Heading, Divider
} from '#adobe/react-spectrum';
type Props = {
heading: string,
content : any,
buttonList: {label: string, clickHandler: Function, closesModal: boolean}[],
isOpen: boolean
}
const Modal: FC<Props> = ( props ) =>
{
const [open, setOpen] = useState(props.isOpen);
let buttons = props.buttonList.map((button, index) =>
{
return <Button key={index} variant="cta" onPress={() => close(button.clickHandler, button.closesModal)} autoFocus>
{button.label}
</Button>
});
const close = (clickHandler: Function | null, closesModal: boolean) =>
{
if(clickHandler != null)
{
clickHandler()
}
if(closesModal)
{
setOpen(false)
}
}
return (
<DialogContainer onDismiss={() => close(null, true)} >
{open &&
<Dialog>
<Heading>{props.heading}</Heading>
<Divider />
<Content>{props.content}</Content>
<ButtonGroup>
{buttons}
</ButtonGroup>
</Dialog>
}
</DialogContainer>
);
}
export default Modal;
Following Firefighters suggestion I get an error now:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
at resolveDispatcher (react.development.js:1476)
at useContext (react.development.js:1484)
at useProvider (module.js:239)
at $bc3300334f45fd1ec62a173e70ad86$var$Provider (module.js:95)
at describeNativeComponentFrame (react-dom.development.js:946)
at describeFunctionComponentFrame (react-dom.development.js:1034)
at describeFiber (react-dom.development.js:1119)
at getStackByFiberInDevAndProd (react-dom.development.js:1138)
at createCapturedValue (react-dom.development.js:20023)
at throwException (react-dom.development.js:20351)
Try putting the open state inside the App component and remove it from the Modal component:
const [errors, setErrors] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if(errors.length > 0) setIsModalOpen(true);
}, [errors])
<Modal
...
isOpen={isModalOpen}
setIsOpen={setIsModalOpen}
/>

Jest - Testing modals in React gives error

I am using react-test-renderer with Jest to test react components. But if I test a react-mui modal dialog like this:
describe('Dashboard', function () {
let dashboard;
beforeEach(async () => {
testRenderer = TestRenderer.create(<MemoryRouter><Route component={Dashboard} /></MemoryRouter>);
dashboard = testRenderer.root.findByType(Dashboard);
await waitForExpect(() => expect(dashboard.instance.state.hasLoaded).toBeTruthy());
});
it('opens dialog on clicking the new class', async () => {
const button = testRenderer.root.findByType(Button);
expect(dashboard.instance.state.showDialog).toBeFalsy();
button.props.onClick();
expect(dashboard.instance.state.showDialog).toBeTruthy();
});
});
But, then I get an error:
Error: Failed: "Error: Uncaught 'Warning: An invalid container has
been provided. This may indicate that another renderer is being used
in addition to the test renderer. (For example, ReactDOM.createPortal
inside of a ReactTestRenderer tree.) This is not supported.%s'
How should I test then react portals to make this test work?
Try putting this in your tests:
beforeAll(() => {
ReactDOM.createPortal = jest.fn((element, node) => {
return element
})
});
Based on Oliver's answer, but for TypeScript users:
describe("Tests", () => {
const oldCreatePortal = ReactDOM.createPortal;
beforeAll(() => {
ReactDOM.createPortal = (node: ReactNode): ReactPortal =>
node as ReactPortal;
});
afterAll(() => {
ReactDOM.createPortal = oldCreatePortal;
});
});
For me, the existing solutions don't address the root cause.
I needed to add jest mocks for all the sub-components in the component I was testing.
For example, consider this JSX that I want to test:
import { CustomTextInput } from 'components/CustomTextInput';
import { CustomButton } from 'components/CustomButton';
return (
<>
<CustomTextInput />
<CustomButton />
</>
)
I need to add mocks for CustomTextInput and CustomButton in my test file like this:
jest.mock(
'components/CustomTextInput',
() => ({ default: 'mock-CustomTextInput' }),
);
jest.mock(
'components/CustomButton',
() => ({ default: 'mock-CustomButton' }),
);

Resources