Test for alerts with react-native-testing-library - react-native

Can react-native-testing-library find an alert, created with Alert.alert()?
My app creates the alert as expected but this test is failing:
// test
const Wrapper = props => (
<Fragment>
<SubscriptionProductDetailScreen
product={product}
testID={"SUBSCRIPTION_DETAIL_SCREEN"}
addToCart={addToCartSpy}
{...props}
/>
</Fragment>
);
function createWrapper(customProps) {
const wrapper = render(<Wrapper {...customProps} />);
return wrapper;
}
beforeEach(() => {
wrapper = createWrapper();
});
// later, inside a describe block:
it('should show an alert if no bars are selected', async () => {
pressSubmitButton()
expect(addToCartSpy).not.toHaveBeenCalled()
// const alert = await waitForElement(
// wrapper.queryByText("Please select up to 4 free items.")
// )
const alert = wrapper.queryByText("Please select up to 4 free items.")
expect(alert).not.toBeNull()
});
// brief excerpt from the component (the onPress handler for the submit button)
addToCart() {
const freeItems = this.state.items[0]
if (!freeItems || !freeItems.selections.length) {
Alert.alert("Error", "Please select up to 4 free items.")
return
}
const item: {...}
this.props.addToCart(item)
}
The async version (waitForElement, commented) also fails.
Again, the alert works in the app itself, and the assertion that the dispatch action, called by the handler, passes.

Alert is not within a React application stack - it's a system feature, so it can't be directly tested with react-native-testing-library. You can however at least verify if it was executed:
import { Alert } from 'react-native'
jest.mock('react-native', () => {
const RN = jest.requireActual('react-native')
return Object.setPrototypeOf(
{
Alert: {
...RN.Alert,
alert: jest.fn(),
},
},
RN,
)
})
test('...', () => {
// ...
expect(Alert.alert).toHaveBeenCalled()
})

Related

Testing custom hook - not wrapped in act warning

I' trying to test a custom hook but I receive this warning message
console.error node_modules/#testing-library/react-hooks/lib/core/console.js:19
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
This is my custom hook
import { useState, useEffect } from 'react'
import io from 'socket.io-client'
import config from './../../../../config'
const useNotificationsSocket = (user) => {
const [socket, setSocket] = useState(null)
const [numUnreadMessages, setNumUnreadMessages] = useState(0)
const configureSocket = socket => {
socket.on('connect', () => {
const data = {
user: user,
}
socket.emit('user joined', data)
})
socket && socket.on('messages updated', (data) => {
//console.log(data)
setNumUnreadMessages(data.numUnreadMessages)
})
}
useEffect(() => {
const fetchSocket = async () => {
const s = await io(config.nSocket.url, {transports: ['websocket']})
configureSocket(s)
setSocket(s)
}
// Check that user is not an empty object as this causes a crash.
user && user.Id && fetchSocket()
}, [user])
return [socket, numUnreadMessages]
}
export { useNotificationsSocket }
and this is the test
import { renderHook, act } from '#testing-library/react-hooks'
import { useNotificationsSocket } from './../hooks/useNotificationsSocket'
jest.mock('socket.io-client')
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
expect(result).not.toBeNull()
})
})
I've tried importing act and wrapping the code in a call to act but however I try to wrap the code I still get a warning and can't figure out how I should use act in this case.
Your hook is asynchronous, so you need to await its response:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
})
})
Additionally, if you define multiple tests, you may encounter your original error if you fail to unmount the hook. At least this appears to be the behaviour in #testing-library/react v13.3.0. You can solve this by unmounting the hook when your test completes:
describe('useNotificationsSocket', () => {
it('returns a socket and numUnreadMessages', async () => {
const user = { Id: '1' }
const { result, unmount } = renderHook(() => useNotificationsSocket(user))
await waitFor(() => expect(result).not.toBeNull())
unmount()
})
})

Async custom hook from within useEffect

When kept in the component body, the following code works fine. Inside useEffect, it checks the asyncstorage and dispatches an action (the function is longer but other checks/dispatches in the function are of the same kind - check asyncstorage and if value exists, dispatch an action)
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
I'm trying to move it to a custom hook but am having problems. The custom hook is:
const useGetUserSettings = () => {
const dispatch = useDispatch();
useEffect(() => {
const getSettings = async () => {
const aSet = await AsyncStorage.getItem('aSet');
if (aSet) {
dispatch(setASet(true));
}
};
getSettings();
}, [dispatch]);
};
export default useGetUserSettings;
Then in the component where I want to call the above, I do:
import useGetUserSettings from './hooks/useGetUserSettings';
...
const getUserSettings = useGetUserSettings();
...
useEffect(() => {
getUserSettings();
}, [getUserSettings])
It returns an error:
getUserSettings is not a function. (In 'getUserSettings()', 'getUserSettings' is undefined
I've been reading rules of hooks and browsing examples on the internet but I can get it working. I've got ESlint set up so it'd show if there were an invalid path to the hook.
Try the following.
useEffect(() => {
if (!getUserSettings) return;
getUserSettings();
}, [getUserSettings]);
The hook doesn't return anything, so it's not surprising that the return value is undefined ;)

how to handle the return values from reducer, inside a functional component

This may be a basic question, but I'm new to React Native and stuck here.
My code pasted below. reducer and functional component. I want to capture the response returned from reducer.
reducer.js
export const ActivationCenterReducer = (
state = INIT_KIT_STATE,
{ type, payload = {} }
) => {
switch (type) {
case 'KIT_ACTIVATION_SUCCESS_DATA': {
const { message, response_code, apiLoading, apiError } = payload;
return {
...state,
apiLoading: apiLoading,
apiError: apiError,
message: message,
response_code: response_code
};
}
// ...
}
// ...
};
Functional Component class:
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
const handleKitActivation = () => {
/*This will call the validation() inside action.js and that follows the reducer.js file. where reducer.js file returning the values on success response. but I am not able to access that response_code returned from reducer.
How to save the response_code from the below dispatch function.*/
dispatch(Validation(locator, pin));
if (response_code === 200) {
// should navigate to the next screen
}
};
};
My question is how to capture the returned response_code from reducer.
I'm able to navigate to the next screen on clicking the submit button couple of times.I notice that first time when the dispatch function is called, the state of the response_code is not updating , hence the response_code != 200.
I want a way to capture the response and assign to variable.
Thanks in advance.
You are probably looking at the old value of response_code in your handleKitActivation.
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
const handleKitActivation = () => {
dispatch(Validation(locator, pin));
// HERE the response_code does not have result value
// of your calling dispatch(Validation(locator, pin)) above yet
if (response_code === 200) {
// should navigate to the next screen
}
};
};
I suggest to move your response_code hanfling to the useEffect:
const kitActivationCenter = ({ route, navigation }) => {
const response_code = useSelector(
store => store.kitActivationCenter.response_code
);
// this effect will run whenever your response_code changes
useEffect(() => {
if (response_code === 200) {
// should navigate to the next screen
}
}, [response_code]);
const handleKitActivation = () => {
dispatch(Validation(locator, pin));
};
};

React Native testing - act without await

Below test is passing but I get the following warning twice and I don't know why. Could someone help me to figure it out?
console.error
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);
at printWarning (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:120:30)
at error (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:92:5)
at ../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14953:13
at tryCallOne (../../node_modules/react-native/node_modules/promise/lib/core.js:37:12)
at ../../node_modules/react-native/node_modules/promise/lib/core.js:123:15
at flush (../../node_modules/asap/raw.js:50:29)
import { fireEvent } from '#testing-library/react-native'
import { renderScreen } from 'test/render'
describe('screens/home', () => {
it('should render and redirect to the EventScreen', async () => {
const {
getByA11yLabel,
findByA11yLabel,
findAllByA11yLabel,
toJSON
} = renderScreen('Main')
expect(toJSON()).toMatchSnapshot('Default render')
const title = 'New event'
const titleInput = getByA11yLabel('event.title')
// Change title - sync fn
fireEvent.changeText(titleInput, title)
// Create button should be visible
const createButton = await findByA11yLabel('event.create')
expect(titleInput.props.value).toBe(title)
expect(createButton).toBeTruthy()
expect(toJSON()).toMatchSnapshot('Change title')
// Create event - async fn
fireEvent.press(createButton)
// The app should be redirected to the EventScreen
const titleInputs = await findAllByA11yLabel('event.title')
const upsertButton = await findByA11yLabel('event.upsert')
expect(toJSON()).toMatchSnapshot('Create event')
expect(titleInputs).toHaveLength(2)
expect(titleInputs[0].props.value).toBe('') // #MainScreen
expect(titleInputs[1].props.value).toBe(title) // #EventScreen
expect(upsertButton).toBeTruthy()
})
})
As far as I know, there is no need to wrap fireEvent with an act- link
findBy* also are automatically wrapped with act - link
Related issue in GitHub is still open
Dependencies:
react: 16.13.1
expo: 39.0.4
jest: 26.6.3
ts-jest: 26.4.4
jest-expo: 39.0.0
#testing-library/jest-native: 3.4.3
#testing-library/react: 11.2.2
#testing-library/react-native: 7.1.0
react-test-renderer: 16.13.1
typescript: 4.1.2
If you've exhausted all other debugging efforts and are pretty sure your code is written correctly, it may be related to react-native/jest-preset replacing global.Promise with a mock (see issue).
The solution to the problem, in this case, is to override/patch the jest preset to first save the original global Promise, apply the react-native/jest-preset and then restore the original Promise (overwriting the mocked version). This allowed me to use await in the tests that were unrelated to rendering without triggering the dreaded
console.error
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);
This snippet shows one way to perform this patch: https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938
I was facing the same problem. For my case I was using useEffect in my component. And while test it prompted me to wrap the rendering inside an act() call. Once I did that i.e. act(async () => ...) my initial problem was solved but I was getting the above mentioned error (Warning: You called act(async () => ...) without await.). I had to use await act(async () => ...) in my test to fix that. Though I am still not sure why it was required.
For reference I am adding a complete example component and corresponding test using await act(async () => ...);
LocationComponent.tsx
/** #jsx jsx */
import { jsx } from 'theme-ui';
import { FunctionComponent, useEffect, useState } from 'react';
type Coordinate = {
latitude: number;
longitude: number;
};
const LocationComponent: FunctionComponent<any> = () => {
const [coordinate, setCoordinate] = useState<Coordinate>();
const [sharedLocation, setSharedLocation] = useState<boolean>();
useEffect(() => {
let mounted = true;
if (!coordinate && navigator) {
navigator.geolocation.getCurrentPosition(function (position) {
setCoordinate({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
});
navigator.permissions
.query({ name: 'geolocation' })
.then(function (result) {
if (mounted) setSharedLocation(result.state === 'granted');
});
}
return () => (mounted = false);
});
return (
<>
<div>Location shared:{sharedLocation ? 'Yes' : 'No'}</div>
<div>Latitude:{coordinate?.latitude}</div>
<div>Longitude:{coordinate?.longitude}</div>
</>
);
};
export default LocationComponent;
LocationComponent.spec.tsx
import React from 'react';
import { render, waitFor } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import LocationComponent from '../../../../../src/components/scheduler/location/LocationComponent';
const TEST_COORDS = {
latitude: 41.8817089,
longitude: -87.643301,
};
global.navigator.permissions = {
query: jest
.fn()
.mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
};
global.navigator.geolocation = {
getCurrentPosition: jest.fn().mockImplementationOnce((success) =>
Promise.resolve(
success({
coords: TEST_COORDS,
})
)
),
};
describe("Location Component when location share is 'granted'", () => {
it('should display current location details', async () => {
await act(async () => {
const { getByText } = render(<LocationComponent />);
/*expect(
await waitFor(() => getByText('Location shared:Yes'))
).toBeInTheDocument();*/
expect(
await waitFor(() => getByText('Latitude:41.8817089'))
).toBeInTheDocument();
expect(
await waitFor(() => getByText('Longitude:-87.643301'))
).toBeInTheDocument();
});
});
});

Promise isn't working in react component when testing component using jest

Good day. I have the following problem:
I have an item editor.
How it works: I push 'Add' button, fill some information, click 'Save' button.
_onSaveClicked function in my react component handles click event and call function from service, which sends params from edit form to server and return promise.
_onSaveClicked implements
.then(response => {
console.log('I\'m in then() block.');
console.log('response', response.data);
})
function and waits for promise result. It works in real situation.
I created fake service and placed it instead of real service.
Service's function contains:
return Promise.resolve({data: 'test response'});
As you can see fake service return resolved promise and .then() block should work immediatly. But .then() block never works.
Jest test:
jest.autoMockOff();
const React = require('react');
const ReactDOM = require('react-dom');
const TestUtils = require('react-addons-test-utils');
const expect = require('expect');
const TestService = require('./service/TestService ').default;
let testService = new TestService ();
describe('TestComponent', () => {
it('correct test component', () => {
//... some initial code here
let saveButton = TestUtils.findRenderedDOMComponentWithClass(editForm, 'btn-primary');
TestUtils.Simulate.click(saveButton);
// here I should see response in my console, but I don't
});
});
React component save function:
_onSaveClicked = (data) => {
this.context.testService.saveData(data)
.then(response => {
console.log('I\'m in then() block.');
console.log('response', response.data);
});
};
Service:
export default class TestService {
saveData = (data) => {
console.log('I\'m in services saveData function');
return Promise.resolve({data: data});
};
}
I see only "I'm in services saveData function" in my console.
How to make it works? I need to immitate server response.
Thank you for your time.
You can wrap your testing component in another one like:
class ContextInitContainer extends React.Component {
static childContextTypes = {
testService: React.PropTypes.object
};
getChildContext = () => {
return {
testService: {
saveData: (data) => {
return {
then: function(callback) {
return callback({
// here should be your response body object
})
}
}
}
}
};
};
render() {
return this.props.children;
}
}
then:
<ContextInitContainer>
<YourTestingComponent />
</ContextInitContainer>
So your promise will be executed immediately.