I have a React Native (non-Expo) app with a screen for reading QR/barcodes. I am using React Native 0.71.8 and react-native-vision-camera, with vision-camera-code-scanner for the frame processor. As part of the flow for accessing a device camera, the app must request permission and/or verify that permission is granted. My camera screen component records these permissions in useState variables. I am struggling to write Jest tests for the screen's behavior when permissions are granted or refused.
So far, the only test I have been able to write successfully is to mock react-native-vision-camera and check that the screen renders. I have mocked Vision Camera's permission and devices APIs, but when I fire a press of the "show camera" button, the test fails.
CameraScreen.tsx
import { useEffect, useState } from 'react';
import { Alert, Linking, StyleSheet, TouchableOpacity, View } from 'react-native';
import { IconButton, Paragraph, Snackbar, useTheme } from 'react-native-paper';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { Camera, CameraDevice, useCameraDevices } from 'react-native-vision-camera';
import { Barcode, BarcodeFormat, useScanBarcodes } from 'vision-camera-code-scanner';
export const CameraScreen = (): JSX.Element => {
const theme = useTheme();
const [isSnackbarVisible, setIsSnackbarVisible] = useState<boolean>(false);
const [isCameraOn, setIsCameraOn] = useState<boolean>(false);
const [hasCameraPermission, setHasCameraPermission] = useState(false);
const [code, setCode] = useState<Barcode | null>();
const [isScanned, setIsScanned] = useState<boolean>(false);
const cameraDevices = useCameraDevices();
const device = cameraDevices.back as CameraDevice;
const handleCloseCamera = async () => {
setIsCameraOn(false);
setIsSnackbarVisible(false);
setIsScanned(true);
setCode(null);
};
const handleOpenCamera = () => {
if (hasCameraPermission) {
setIsScanned(false);
setIsCameraOn(true);
} else {
Alert.alert(
'Camera permission not granted.',
'Open Settings > Apps > DemoApp > Permissions to allow camera access.',
[
{ text: 'Dismiss' },
{
text: 'Open Settings',
onPress: async () => {
Linking.openSettings();
},
},
]
);
}
};
useEffect(() => {
(async () => {
const status = await Camera.requestCameraPermission();
setHasCameraPermission(status === 'authorized');
})();
/** The frame processor handles each individual image taken by the camera. */
const [frameProcessor, codes] = useScanBarcodes([BarcodeFormat.ALL_FORMATS], {
checkInverted: true,
});
useEffect(() => {
handleCodeRead();
}, [codes]);
const handleCodeRead = async () => {
if (codes && codes.length > 0 && !isScanned) {
codes.forEach(async (scannedCode) => {
setCode(scannedCode);
setIsSnackbarVisible(true);
});
}
};
return (
<View style={styles.container}>
{device && isCameraOn && hasCameraPermission ? (
<View style={styles.container} testID="camera-view">
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
frameProcessor={frameProcessor}
frameProcessorFps={5}
/>
<IconButton style={styles.closeButton} icon="close" size={50} onPress={() => handleCloseCamera()} />
</View>
) : (
<View style={styles.noCameraScreenStyle}>
<TouchableOpacity onPress={handleOpenCamera} style={styles.openButton} testID="open-camera">
<Icon name="camera" size={60} color={theme.colors.text} />
<Paragraph style={styles.text}>Open Scanner</Paragraph>
</TouchableOpacity>
</View>
)}
<Snackbar
testID="snackbar"
visible={isSnackbarVisible}
onDismiss={() => setIsSnackbarVisible(false)}
action={{
label: 'Close Camera',
onPress: () => handleCloseCamera(),
}}
>
{code == null ? '' : `${BarcodeFormat[code.format]} read: \n${code.rawValue as string}`}
</Snackbar>
</View>
);
};
const styles = StyleSheet.create({
closeButton: {
height: 60,
width: 60,
alignSelf: 'flex-start',
},
closeButtonContainer: {
backgroundColor: 'rgba(0,0,0,0.2)',
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
bottom: 0,
padding: 20,
},
openButton: {
alignItems: 'center',
},
container: {
flex: 1,
},
noCameraScreenStyle: {
flex: 1,
justifyContent: 'space-evenly',
},
text: {
fontSize: 18,
marginTop: 15,
paddingBottom: 10,
paddingLeft: 15,
paddingRight: 15,
},
});
CameraScreen.test.tsx
import { fireEvent, render, screen } from '@testing-library/react-native';
import React from 'react';
import renderer, { act } from 'react-test-renderer';
import { CameraScreen } from './CameraScreen';
import { View } from 'react-native';
import { Barcode } from 'vision-camera-code-scanner';
jest.useFakeTimers();
// mock react-native-vision-camera
const mockCamera = () => {
return <View testID="camera" />;
}
jest.mock('react-native-vision-camera', () => {
return {
Camera: {
Camera: mockCamera,
getCameraPermissionStatus: jest.fn(() => Promise.resolve( 'authorized' )),
requestCameraPermission: jest.fn(() => Promise.resolve( 'authorized' )),
},
useCameraDevices: () => {
return {
back: {
deviceId: 'test',
lensFacing: 'back',
position: 'back',
},
front: {
deviceId: 'test',
lensFacing: 'front',
position: 'front',
},
};
},
}
});
// mock vision-camera-code-scanner
let mockedUseScanBarcodes: jest.Mock<{}, []>;
jest.mock('vision-camera-code-scanner', () => {
const barcode: Barcode[] = [];
mockedUseScanBarcodes = jest.fn().mockReturnValue([() => {}, barcode]);
return {
BarcodeFormat: {
ALL_FORMATS: 0,
},
useScanBarcodes: mockedUseScanBarcodes,
};
});
describe('Camera Scanner', () => {
it('should render', async () => {
await act(async () => {
const root = renderer.create(<CameraScreen />);
expect(root.toJSON()).toMatchSnapshot();
})
});
it('should show camera when button is pressed', async () => {
render(<CameraScreen />);
const button = screen.getByTestId('open-camera');
fireEvent.press(button);
const cameraView = await screen.findByTestId('camera-view');
expect(cameraView).toBeTruthy();
});
});
Jest output:
yarn run v1.22.19
$ jest CameraScreen
FAIL screens/CameraScreen/CameraScreen.test.tsx
Camera Scanner
√ should render (210 ms)
× should show camera when button is pressed (163 ms)
● Camera Scanner › should show camera when button is pressed
Unable to find an element with testID: camera-view
<View>
<View>
<View
testID="open-camera"
>
<Text>
</Text>
<Text>
Open Scanner
</Text>
</View>
</View>
</View>
76 | const button = screen.getByTestId('open-camera');
77 | fireEvent.press(button);
> 78 | const cameraView = await screen.findByTestId('camera-view');
| ^
79 | expect(cameraView).toBeTruthy();
80 | });
81 | });
at Object.findByTestId (screens/CameraScreen/CameraScreen.test.tsx:78:37)
at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:24)
at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:22:9)
at node_modules/@babel/runtime/helpers/asyncToGenerator.js:27:7
at Object.<anonymous> (node_modules/@babel/runtime/helpers/asyncToGenerator.js:19:12)