I'm trying to achieve 100% coverage in a project and this is the only file I can't test because I haven't got any idea of how to do it.
I don't even know where to start from.
I'm using Jest and React Testing Library. The project uses NextJS.
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
}
ps: I know coverage isn't the most important thing, but for this project 100% is needed.
Often with NextJS, we need to test 2 cases, the Initial/Server Props part and the React Component part. Yours have only the getInitialProps. Test might differs according to configuration. I'll post my configuration and Tests for both cases for future readers, and hope it can be a solid base to cover most of it at least.
File pages/_document.js
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '#material-ui/core/styles';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Lato"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async ctx => {
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
File __tests__/pages/_document.js
Before Posting the Test file, one thing that is very important is to Stub the context, ctx in MyDocument.getInitialProps = async ctx => { and mock the ctx.renderPage that will be backed up in document codes and called. The result of this call is another function that also need to be called in other to reach maximum coverage in that section. To get a hint on what to use, you can simply log the ctx inside the document and see wha the function looks like. The stub and mock can be like this:
const ctx = {
renderPage: (options = {}) => {
// for coverage, call enhanceApp and App
if (typeof options.enhanceApp === 'function') {
// pass a functional component as parameter
const app = options.enhanceApp(() => <div>App Rendered</div>);
app();
}
return {
html: <div>App Rendered</div>,
head: (
<head>
<title>App Title</title>
</head>
),
};
},
};
Here is the full test file, which also handle Shallow Rendering:
import { createShallow } from '#material-ui/core/test-utils';
import MockProviders from '../../tests/MockProviders';
import MyDocument from '../../pages/_document';
/** #test {Document Component getInitialProps} */
describe('Document Component getInitialProps', () => {
const ctx = {
asPath: '/', // not necessary, but useful for testing _app.js
res: {
writeHead: jest.fn(),
end: jest.fn(),
}, // not necessary but useful for testing other files
renderPage: (options = {}) => {
// for coverage, call enhanceApp and App
console.log('options', options);
if (typeof options.enhanceApp === 'function') {
const app = options.enhanceApp(() => <div>App Rendered</div>);
console.log('app', app);
app();
}
return {
html: <div>App Rendered</div>,
head: (
<head>
<title>App Title</title>
</head>
),
};
},
};
it('should return finalize html, head and styles in getInitialProps', async () => {
const result = await MyDocument.getInitialProps(ctx);
// Log to see the structure for your assertion if any expectation
// console.log(result);
expect(result.html.props.children).toBe('App Rendered');
expect(result.head.props.children.props.children).toBe('App Title');
expect(result.styles[0].props.id).toBe('jss-server-side');
});
});
/** #test {Document Component} */
describe('Document Component', () => {
const shallow = createShallow();
const wrapper = shallow(
<MockProviders>
<MyDocument />
</MockProviders>
);
const comp = wrapper.find('MyDocument').dive();
// console.log(comp.debug());
it('should render Document components Html', () => {
expect(comp.find('Html')).toHaveLength(1);
expect(comp.find('Head')).toHaveLength(1);
expect(comp.find('body')).toHaveLength(1);
expect(comp.find('Main')).toHaveLength(1);
expect(comp.find('NextScript')).toHaveLength(1);
});
});
EDIT 1-------
My MockProviders file is just for code factorization, instead of adding Providers components in cascade on each test, and later to change all test files if you need to add another Provider, then you would only to change that one MockProvider file. It's a king of mocking itself because you inject your own props on it while testing, which is different from the normal value you might inject on the real application.
import { MuiThemeProvider } from '#material-ui/core/styles';
import { StateProvider } from '../src/states/store';
import theme from '../src/themes';
const MockProviders = props => {
return (
<StateProvider {...props}>
<MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>
</StateProvider>
);
};
export default MockProviders;
Because I used a Provider for managing state with React.useContext and a Provider for MaterialUI theme, then I add both of them in cascade, with ability to pass additional props, and render the children components inside.
Related
I'm trying to write a test case for testing URL in my react native app this is my mock
import { Linking } from "react-native";
jest.mock('react-native/Libraries/Linking/Linking', () => {
return {
openURL: jest.fn()
}
})
Linking.openURL.mockImplementation(() => true)
and this is my test
test('open google url',async ()=>{
expect(Linking.openURL()).toHaveBeenCalled('https://www.google.com/')
})
but I get this error what should I do?
Name the mock function in a constant and then test if that function has been called. Here's how you would set up:
import * as ReactNative from "react-native";
const mockOpenURL = jest.fn();
jest.spyOn(ReactNative, 'Linking').mockImplementation(() => {
return {
openURL: mockOpenURL,
}
});
and then you can test this way (my example uses react-testing-library, but you can use whatever). Note you should use toHaveBeenCalledWith(...) instead of toHaveBeenCalled(...)
test('open google url', async () => {
// I assume you're rendering the screen here and pressing the button in your test
// example code below
const { getByTestId } = render(<ScreenToTest />);
await act(async () => {
await fireEvent.press(getByTestId('TestButton'));
});
expect(mockOpenURL.toHaveBeenCalledWith('https://www.google.com/'));
});
If I understoof your question then you can use react-native-webview.
import WebView from 'react-native-webview';
export const WebView: React.FC<Props> = ({route}) => {
const {url} = route.params;
<WebView
source={{uri: url}}
/>
);
};
This is how I use my webview screen for any url I need to open (like terms and conditions, etc...)
I am trying to show in the app that I built in React a PDF file using PDFtron and encounter the following error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer.
my code is:
import { url } from "../../../../config.json";
import React, { useState, useEffect, useRef } from "react";
import { getProject } from "../../../../services/projectService";
import { useParams } from "react-router-dom";
import WebViewer from "#pdftron/webviewer";
import { getCurrentUser } from "../../../../services/userService";
import { Link, Redirect } from "react-router-dom";
import { deleteImage } from "../../../../services/projectService";
const MyContracts = () => {
const [project, setProject] = useState({});
const [counter, setCounter] = useState(0);
const [files, setFiles] = useState([]);
const { id } = useParams();
// const viewerDiv = useRef();
const user = getCurrentUser();
const [viewerUrl, setViewerUrl] = useState(`${url}/files/testing.pdf`);
const viewer = document.getElementById("viewer");
useEffect(() => {
getProject(id)
.then(res => {
setProject(res.data);
setFiles(res.data.files.contracts);
})
.catch(error => console.log(error.message));
}, []);
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
if (!user) return <Redirect to="/private-area/sign-in" />;
if (user && user.isAdmin | (user._id === project.userID))
return (
<div className="container">
</div>
{/********** PDF VIEWER ************/}
<div className="web-viewer" id="viewer"></div>
{/* <div className="web-viewer" ref={viewerDiv} id="viewer"></div> */}
{/********** PDF Gallery ************/}
{files !== undefined && (
<>
<h2 className="text-rtl h3Title mt-2">בחר קובץ</h2>
<select
id="select"
className="col-12 text-rtl px-0"
onChange={e => setViewerUrl(e.target.value)}>
{files.map((file, index) => (
<option value={`${url}${file.url}`} key={index}>
{file.name}
</option>
))}
</select>
</>
)}
</div>
);
};
export default MyContracts;
What am I doing wrong and how can I fix it?
I see that you are trying to load multiple instances of WebViewer:
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
Webviewer cannot be instantiated more than once in the same HTML element. If you need a completely different instance, you can hide or remove the HTML element and create a new one to hold the new instance.
That being said, if you just need to load another document, I would recommend using the loadDocument API. You can read more about it here as well.
I am trying to navigate my app from outside of a component. Specifically, I am using a fetch interceptor and I want to navigate whenever an error response is received.
I followed the example here: https://reactnavigation.org/docs/navigating-without-navigation-prop/
However, my app is still giving me an error saying that either a navigator isn't rendered or the navigator hasn't finished mounting:
Screenshot of app with error message
As far as I can tell, neither of those situations apply. The app is loaded and rendered with a navigator in place before I try to actually navigate
My App.jsx:
// ... imports and so on ...
fetchIntercept.register({
response: (response) => {
if (response.status === 401) {
// Unverified subscription
RootNavigation.reset({ index: 0, routes: [{ name: 'Intercept' }] });
}
return response;
},
});
{ ... }
const InterceptNavigator = createStackNavigator(
{
Application: {
screen: ApplicationScreen,
},
Intercept: {
screen: SubscriptionInterceptScreen,
},
},
{
initialRouteKey: 'Application',
},
);
const App = createAppContainer(InterceptNavigator);
export default () => {
React.useEffect(() => {
RootNavigation.isMountedRef.current = true;
return () => { RootNavigation.isMountedRef.current = false; };
}, []);
return (
<NavigationContainer ref={RootNavigation.navigationRef}>
<App />
</NavigationContainer>
);
};
RootNavigation.js:
import * as React from 'react';
export const isMountedRef = React.createRef();
export const navigationRef = React.createRef();
export function navigate(name, params) {
if (isMountedRef.current && navigationRef.current) {
navigationRef.current.navigate(name, params);
}
}
export function reset(options) {
if (isMountedRef.current && navigationRef.current) {
navigationRef.current.reset(options);
}
}
I also inserted a number of console logs throughout and all of them showed that the app is loaded, that the navigationRef is current, and that the isMountedRef is also current before the app tries to navigate
Try .resetRoot() instead of .reset(). I think .reset() needs a state as an argument.
Found the solution. The issue is that I had a mixture of version 4 and version 5 code (and was referring to mixed documentation).
To fix the issue I removed references to version 5 code and then followed the steps on this page to get the navigator working: https://reactnavigation.org/docs/4.x/navigating-without-navigation-prop/
I am trying to test calling a component method from a React Native Button element.
For some reason, the test fails unless I do BOTH of these things.
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
If I comment out either of the lines, the test fails indicating that expect(instance.toggleEmailPasswordModal).toHaveBeenCalled(); failed.
Here is my component:
import React, { Component } from 'react';
import { Button, SafeAreaView, Text } from 'react-native';
import EmailPasswordModal from './EmailPasswordModal/EmailPasswordModal';
class Login extends Component {
state = {
emailPasswordModalVisible: false,
};
toggleEmailPasswordModal = () => {
console.log('TOGGLED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
const { emailPasswordModalVisible } = this.state;
this.setState({ emailPasswordModalVisible: !emailPasswordModalVisible });
};
render() {
const { emailPasswordModalVisible } = this.state;
return (
<SafeAreaView>
<EmailPasswordModal
visible={ emailPasswordModalVisible }
close={ this.toggleEmailPasswordModal }
/>
<Text>Login Screen!</Text>
<Button
onPress={ this.toggleEmailPasswordModal }
title="Login with Email and Password"
color="#841584"
accessibilityLabel="Login with Email and Password"
/>
</SafeAreaView>
);
}
}
export default Login;
Here is my test:
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import { shallow } from 'enzyme';
import { Button } from 'react-native';
import Login from './Login';
describe('Login Screen', () => {
describe('Snapshot Tests', () => {
it('renders the screen with default state', () => {
const renderer = new ShallowRenderer();
const props = {};
renderer.render(<Login { ...props } />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
});
describe('Functional Tests', () => {
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
});
});
});
Oddly, when the test runs, the output shows "TOGGLED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" twice because of the logging in the component.
However, if I change the expect to :
expect(instance.toggleEmailPasswordModal).toHaveBeenCalledTimes(1);
the test passes.
If I change the expect to :
expect(instance.toggleEmailPasswordModal).toHaveBeenCalledTimes(2);
the test fails saying toggleEmailPasswordModal was only called 1 time.
Why do I need BOTH of those wrapper.find(Button)... lines? I've never seen any other tests requiring both of them.
Thanks,
Justin
UPDATE:
I updated my test as follows:
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
wrapper.find(Button).first().simulate('press');
expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
// I ADDED THIS SECTION HERE
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
The test fails because instance.state.emailPasswordModalVisible = false. That's strange as the toggleEmailPasswordModal apparently is called. However, since I suspect it's actually being called twice, I update the test as follows:
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
jest.spyOn(instance, 'toggleEmailPasswordModal');
wrapper.find(Button).first().props().onPress();
// CHANGES START HERE
// wrapper.find(Button).first().simulate('press');
// expect(instance.toggleEmailPasswordModal).toHaveBeenCalled();
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
Guess what? The test passes properly. So CLEARLY calling the wrapper.find... functions twice truly is calling the toggleEmailPasswordModal method twice. So, why does it fail to detect it if I don't call twice? Why does it improperly believe the method has only been called once?
I have an answer finally. According to Jest spyOn function called, I need to do instance.forceUpdate() to attach the spy to the component.
it('calls the toggleEmailPasswordModal method', () => {
const wrapper = shallow(<Login />);
const instance = wrapper.instance();
const spy = jest.spyOn(instance, 'toggleEmailPasswordModal');
// This is added per https://stackoverflow.com/questions/44769404/jest-spyon-function-called/44778519#44778519
instance.forceUpdate();
wrapper.find(Button).first().props().onPress();
expect(spy).toHaveBeenCalledTimes(1);
expect(instance.state.emailPasswordModalVisible).toBe(true);
});
Now, the test passes!
I used arrow function inside of my React component to avoid binding this context, for example my component look like this;
class Comp extends Component {
_fn1 = () => {}
_fn2 = () => {}
render() {
return (<div></div>);
}
}
How do I test _fn1 and _fn2 function in my test cases? Because these kind of function did not associated with React component itself, so when I do
fnStub = sandbox.stub(Comp.prototype, "_fn1");
it is not going work, since _fn did not bind with Comp.prototype. Thus, how can I test those functions in React if I want to create function with arrow syntax? Thanks!
ES6 functions or arrow functions are not added to the class prototype.
However, there are a couple of ways to test them:-
Test that the functions themselves are called when a suitable event occurs
ES5 functions exist on the class prototype and something like this is possible:
import Component from 'path/to/component';
import { shallow } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const spy = jest.spyOn(Component.prototype, 'handleSubmit');
const wrapper = shallow(<Component />);
...
//Invoke handleSubmit
...
expect(spy).toBeCalled()
});
});
whereas ES6 functions exist on the instance of the mounted component(you can also use shallow)
import Component from 'path/to/component';
import { mount } from 'enzyme';
describe(<Component>, () => {
it('should call handleSubmit', () => {
const wrapper = mount(<Component />);
...
const spy = jest.spyOn(wrapper.instance(), 'handleSubmit');
//update the instance with the new spy
wrapper.instance().forceUpdate();
...
//invoke handleSubmit
expect(spy).toBeCalled()
});
});
Test their functionality by simulating actions that will invoke these functions and test for the expected behavior
Assuming component content such as:
state = {
title: 'Current Title'
};
updateTitle = (event) => {
title = event.target.value;
this.setState({ title });
}
render() {
return (
<div>
<input type="text" value={this.state.title} onChange={this.updateTitle} />
<div>
)
}
Test
...
wrapper.find('input').simulate('change', {target: {value: 'New title'}});
expect(wrapper.state().title).toBe('New Title');
...
I hope this helps.
In general I find it easier to test that these functions have resulted in a correct component state, rather than test the function itself. For example, here is a component that toggles a state variable when a button is clicked:
class MyComponent extends Component {
state = {
toggle: false
}
_fn1 = () => {
this.setState(previousState => ({
toggle: !previousState.toggle
});
}
render() {
const { toggle } = this.state;
return (
<button onClick={this.clickHandler}>
Turn me {toggle ? 'on' : 'off'}
</button>
);
}
}
My preferred approach here would be to test the component as a whole, i.e. the "unit" of the unit test is the component. The test would therefore find the button, simulate a click, and ensure that the correct text is shown. This may not be a textbook unit test, but it achieves the goal of testing the component.
Using sinon/chai/mocha/enzyme:
describe('My Component', () => {
it('alternates text display when the button is clicked', () => {
const wrapper = shallow(<MyComponent />);
expect(wrapper).to.have.text('Turn me off');
wrapper.find('button').simulate('click');
expect(wrapper).to.have.text('Turn me on');
});
});