I'm using Jest with Enzyme, and I have this component which includes a navigate method call:
export class LeadList extends React.Component {
render() {
const { navigate } = this.props.navigation;
return (
<List>
{this.props.data.allLeads.map((lead, i) => {
return (
<ListItem
key={i}
onPress={() =>
navigate('Details', lead.id)
}
/>
// ...
</ListItem>
)})}
</List>
);
}
}
I'm trying to test that it gets called properly, so I threw this together:
const testProps = props => ({
data: {
allLeads: [
{id: 1, name: 'John Doe'},
{id: 2, name: 'Jane Doe'}
],
loading: false,
},
navigation: jest.fn((options, callback) => callback('Details', 1)),
...props,
})
describe('interactions', () => {
let props
let wrapper
beforeEach(() => {
props = testProps()
wrapper = shallow(<LeadList {...props} />)
})
describe('clicking a lead', () => {
beforeEach(() => {
wrapper.find(ListItem).first().prop('onPress')
})
it('should call the navigation callback', () => {
expect(props.navigation).toHaveBeenCalledTimes(1)
})
})
})
Output is:
Expected mock function to have been called one time, but it was called zero times.
What's the right way to handle this? Do I need to use a spy?
EDIT:
I'm getting the same when I change it like so:
const testProps = props => ({
// ...
navigation: {navigate: jest.fn()},
...props,
})
it('should call the navigation callback', () => {
expect(props.navigation.navigate).toHaveBeenCalledTimes(1)
})
Output:
expect(jest.fn()).toHaveBeenCalledTimes(1)
Expected mock function to have been called one time, but it was called zero times.
at Object.<anonymous> (__tests__/LeadList-test.js:48:35)
at tryCallTwo (node_modules/promise/lib/core.js:45:5)
at doResolve (node_modules/promise/lib/core.js:200:13)
at new Promise (node_modules/promise/lib/core.js:66:3)
at Promise.resolve.then.el (node_modules/p-map/index.js:46:16)
at tryCallOne (node_modules/promise/lib/core.js:37:12)
at node_modules/promise/lib/core.js:123:15
You will need a spy to test this. Here is an example test for finding the ForgotPassword button on a LoginScreen and testing that it navigates to the correct screen.
test('Press Forgot Password Button', () => {
const spy = jest.spyOn(navigation, 'navigate')
const wrapper = shallow(
<LoginScreen
navigation={navigation}
error={{}}
onLogin={jest.fn()}
/>,
)
const forgotButton = wrapper.find('Button').at(0)
forgotButton.props().onPress()
expect(spy).toBeCalledWith('ForgotPassword')
})
The prop navigation that is passed to the component is not a function. It's an object that contains a function called navigate.
Ironically, that's exactly what you're using in your component code:
const { navigate } = this.props.navigation;
And so, you'll have to change the navigation prop that you're passing from the test to be:
navigation: {navigate: jest.fn()}
and then in your test:
expect(props.navigation.navigate).toHaveBeenCalledTimes(1)
Edit:
In order to actually get the function to be called, you'll have to simulate a press. Right now the code finds the onPress function, but doesn't invoke it.
To do this you can replace
wrapper.find(ListItem).first().prop('onPress')
with
wrapper.find(ListItem).first().props().onPress()
Related
I'm new to React. Somehow only my first test succeeds, and the rest fail. Even if I make the second test same as the first one, it still fails. If I comment out the first test, the second one succeeds, and then the rest fail with component not being found via the id and not present in the DOM.
Here are my tests:
describe('Given SomeComponent component', () => {
configure({ testIdAttribute: 'id' });
describe('When trying to render with only id prop set', () => {
const property = {
id: '1',
} as Property;
render(<SomeComponent prop={property} />);
it('Should render successfully', () => {
expect(screen.getAllByTestId('chart-1')).toBeTruthy();
});
});
describe('When trying to render without data prop set', () => {
const property = {
id: '1',
name: 'property of test',
} as Property;
render(<SomeComponent prop={property} />);
it('Should render successfully', () => {
expect(screen.getAllByTestId('chart-1')).toBeTruthy();
});
});
});
Tested component: SomeComponent
export interface Prop {
property: Property;
}
export const SomeComponent = ({ property }: Prop): JSX.Element => {
return (
<>
<div id={`chart-${curveProperty.id}`}>haha</div>
</>
);
};
Try this:
describe("Given SomeComponent component", () => {
configure({ testIdAttribute: "id" });
it("Should render successfully", () => {
const property = {
id: "1",
} as Property;
const { rerender } = render(<SomeComponent prop={property} />);
expect(screen.getAllByTestId("chart-1")).toBeTruthy();
const newProperty = {
id: "1",
name: "property of test",
} as Property;
rerender(<SomeComponent prop={newProperty} />);
expect(screen.getAllByTestId("chart-1")).toBeTruthy();
});
});
The solution was to use jest test instead of it and it started to work just I wanted it to.
In case switching from it to test doesn't work as it didn't for me:
If the <SomeComponent/> is slightly more complex there could be things still happening at the moment you call getAllByTestId. In my case - switching to the findAllByTestId did the job and all tests are passing as expected.
I am actually using react navigation https://reactnavigation.org/, I have a Component with a method:
class Sing extends Component {
singASong = () => {
console.log('hello i am singing');
}
}
this component will be rendered with react-navigation and a custom header, the question is: how to call the method singASong from the custom header?:
const routeStackNavigation = {
SingingMode: {
screen: Sing,
navigationOptions: ({ navigation }) => ({
header: (
<CustomNav onPressAction={() =>{
// how to call the method singASong from here ?
}
}
/>
),
}),
},
};
UPDATE
I set and test the value with this:
this.props.navigation.setParams({'onPress': 'akumen' });
setTimeout(() => {
console.log(this.props.navigation.getParam('onPress')); // akumen
}, 2000);
And i tests values with:
onPress={() =>{
console.log('NAVIGATION',navigation);
console.log('ON PRESS',navigation.getParam('onPress'));
console.log('PARAMS',navigation.state.params);
return navigation.getParam('onPress');
}
}
but get undefined
FOR STACK NAVIGATION
Below I share pic with my real world example, it was a stack navigation and I did not notice before, but param it just set under the right stack, so take this as motivation to explore:
You can use navigation params:
In componentDidMount
this.props.navigation.setParams({onPressAction: ()=>this.singASong()})
In navigationOptions
<CustomNav onPressAction={navigation.getParam("onPressAction") }/>
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' }),
);
I have a component and I want to test the click method. I am using shallow but my test is failing as it cannot find the button and hence it`s click method. What is wrong with my code?
interface IProps {
label: string;
className: string;
onClick: () => void;
}
export const NewButton: React.StatelessComponent<IProps> = props => {
return (
<Button type="button" className={props.className} onClick={props.onClick}>
{props.label}
</Button>
);
};
import { shallow } from 'enzyme';
import * as React from 'react';
import { NewButton } from "../Buttons";
describe('<NewButton />', () => {
describe('onClick()', () => {
const props = {
className: "buttonSubmit",
label: "submit",
onClick: () => {},
}
test('successfully calls the onClick handler', () => {
const mockOnClick = jest.fn();
const wrapper = shallow(
<NewButton {...props} />
);
const button = wrapper.find('submit').dive();
expect(button.exists()).toEqual(true)
button.simulate('click');
expect(mockOnClick.mock.calls.length).toBe(1);
});
});
});
Since you are using shallow method, it will only render the component that we are testing. It does not render child components. So you should try to find the Button component.
const button = wrapper.find('Button');
After that you should mock the props.onClick event handler passed as props to NewButton component.
const props = {
className: "buttonSubmit",
label: "submit",
onClick: jest.fn(),
}
So you can use
describe('<NewButton />', () => {
describe('onClick()', () => {
const props = {
className: "buttonSubmit",
label: "submit",
onClick: jest.fn(),
}
test('successfully calls the onClick handler', () => {
const wrapper = shallow(
<NewButton {...props} />
);
const button = wrapper.find('Button');
expect(button.exists()).toEqual(true)
button.simulate('click');
// Since we passed "onClick" as props
// we expect it to be called when
// button is clicked
// expect(props.onClick).toBeCalled();
expect(props.onClick.mock.calls.length).toBe(1);
});
});
});
I am using react-native with redux. I am trying to update current screen's params so that they can be accessed in a component used in top-bar but parameter is not getting set.
My code is following:
Screen Route:
AlertNameForm: {
screen: AlertNameForm,
navigationOptions: ({navigation}) => CancelAndDone(navigation)
}
Component Screen: In componentDidMount I am setting parameter.
class AlertNameForm {
..........
componentDidMount() {
this.props.navigation.setParams({onDonePress: this.onDonePress})
}
onDonePress: () => {
// want to access this function in top-bar buttons.
}
}
Following is further components:
export const CancelAndDone = (navigation) => ({
headerLeft: <ButtonCancel navigation={navigation} />,
headerRight: <ButtonDone navigation={navigation} />
})
const ButtonDone = withTheme(({navigation, theme: { tertiaryColor } }) => (
<Button color={tertiaryColor} title="Done" onPress={() => {
if (navigation.state.params && navigation.state.params.onDonePress) {
navigation.state.params.onDonePress()
}
else {
navigation.dispatch(NavigationActions.back())
}
}} />
))
But in ButtonDone component I am not able to access function onDonePress
Is there any other way to setParams for current screen in react-native.
You should reference navigation.state.paramsusing this.props since navigation should be passed as a prop to that component.
You can assign the function within the target component as follows:
componentDidMount = () => {
const { navigation } = this.props
navigation.setParams({
onDonePress: () => this.myFunction(),
})
}
myFunction = () => { /*body function*/ }
In your header or footer component call:
navigation.state.params.onDonePress or route.params.onDonePress if you using React Navigation v5.