How to mock expo-camera requestCameraPermissionsAsync() response? - react-native

I am trying to do a unit test to one of my react native components.
One scenario requires me to mock expo-camera requestCameraPermissionsAsync() method but don't know how. What I'm trying to do is to mock the status to always have the granted value.
Initial approach, below:
jest.mock('expo-camera', () => {
const PermissionsCamera = jest.requireActual('expo-camera');
return {
...PermissionsCamera,
requestCameraPermissionsAsync: () =>
new Promise(resolve => resolve({granted: true, status: 'granted'})),
};
});
But that doesn't work. Need help, is there something wrong with the code above? Thank you
Update:
How I implemented in the component:
import {Camera} from 'expo-camera'
useEffect(() => {
(async () => {
const {status} = await Camera.requestCameraPermissionsAsync();
// additional logic when status is equal to 'granted'
})();
}, []);

Related

TypeError: (0 , _stripeReactNative.initStripe) is not a function #1141

I am trying to write a test case. but I am trying to mock stripe npm but not getting success.
Write two basic tests and execute them you can see the error in the console
App.js file
useEffect(() => {
getStripeToken();
initializeStripe();
}, []);
const initializeStripe = () => {
initStripe({
publishableKey:'pk_te9ms7qP6N70007AHphRr',
merchantIdentifier: 'merchant.identifier',
urlScheme: 'your-url-scheme'
});
};
App.test.tsx
test('Stripe Payment render correctly', () => {
render(<StripePayment />);
});
test('Stripe Payment snapshot capture correctly', () => {
const componentTree = render(<StripePayment />).toJSON();
expect(componentTree).toMatchSnapshot();
});
jest.setup.js
import mock from '#stripe/stripe-react-native/jest/mock.js';
jest.mock('#stripe/stripe-react-native', () => mock);
Received following error

React Native: 'Jest did not exit one second after the test run has completed' with #testing-library/react-hooks and react-query

I am using jest and #testing-library/react-hooks to test hooks implemented with react-query in my React Native code.
The tests work ok, but at the end, I am getting:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
Here is my simplified code:
import { renderHook } from '#testing-library/react-hooks'
import React from 'react'
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const useSomething = () => {
return useQuery('myquery', () => 'OK')
}
beforeAll((done) => {
done()
})
afterAll((done) => {
done()
})
// test cases
describe('Testing something', () => {
it('should do something', async () => {
const queryClient = new QueryClient()
const wrapper = ({ children }: { children: React.ReactFragment }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result, waitFor } = renderHook(() => useSomething(), { wrapper })
await waitFor(() => {
return result.current.isSuccess
})
expect(result.current.data).toBe('OK')
})
})
I tried using cleanup, done, unmount, etc. before each/all with no results. If I remove useQuery from useSomething, the problem disappears.
Any idea how to fix it?
This issue has been reported in the past here:
https://github.com/tannerlinsley/react-query/issues/1847
The issue is caused by the react-query garbage collection timer running, which defaults to 5 minutes. Solutions are, as described in the issue:
clearing the queryCache after each test:
afterEach(() => { queryClient.clear() });
setting cacheTime to 0 for your test, e.g. with: queryClient.setDefaultOptions({ queries: { cacheTime: 0 } })
using jest.useFakeTimers()
You could try defining a function like:
export function flushPromises() {
return new Promise((resolve) => setImmediate(resolve));
}
Then on your test before the expect:
await flushPromises();
More info here

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 ;)

Fetch more data in RxJs

I have some problem with apply fetching "more" data using fromFetch from rxjs.
I have project with React and RXJS. Currently I'm using something like this:
const stream$ = fromFetch('https://pokeapi.co/api/v2/pokemon?limit=100', {
selector: response => response.json()
}).subscribe(data => console.log(data));
But! I would like to change limit dynamically, when I click button or even better - when I scroll to the very bottom of my website. How to make something like this?
So that, based on some interaction, the limit would change?
The way your observable work in your case it's a request-response. You're declaring stream$ to be an observable that when someone subscribes it will make a request with limit=100.
There are different ways of solving this... The most straightforward would be:
const getPokemon$ = limit =>
fromFetch('https://pokeapi.co/api/v2/pokemon?limit=' + limit, {
selector: response => response.json()
});
const MyComponent = () => {
// ...
useEffect(() => {
const sub = getPokemon$(limit).subscribe(res => console.log(res));
return () => sub.unsubscribe();
}, [limit])
// ...
}
Another option, probably a bit more reactive but harder to follow for others, would be to declare another stream which sets the limit:
const limit$ = new BehaviorSubject(100)
const pokemon$ = limit$.pipe(
switchMap(limit => fromFetch('https://pokeapi.co/api/v2/pokemon?limit=' + limit, {
selector: response => response.json()
}))
);
// In your component
const MyComponent = () => {
// ...
useEffect(() => {
const sub = pokemon$.subscribe(res => console.log(res));
return () => sub.unsubscribe();
}, [])
changeLimit = (newLimit) => limit$.next(newLimit)
// ...
}
In this other solution, you're declaring how pokemon$ should react to changes on limit$, and you can set limit$ from any other component you want.

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();
});
});
});