import { Video } from 'expo-av'; import { Camera } from 'expo-camera'; import React from 'react'; import { Platform } from 'react-native'; import { waitFor, mountAndWaitFor as originalMountAndWaitFor, retryForStatus } from './helpers'; import * as TestUtils from '../TestUtils'; export const name = 'Camera'; const style = { width: 200, height: 200 }; export async function test(t, { setPortalChild, cleanupPortal }) { const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; describeWithPermissions('Camera', () => { let instance = null; let originalTimeout; const refSetter = (ref) => { instance = ref; }; const mountAndWaitFor = (child, propName = 'onCameraReady') => new Promise((resolve) => { const response = originalMountAndWaitFor(child, propName, setPortalChild); setTimeout(() => resolve(response), 1500); }); t.beforeAll(async () => { await TestUtils.acceptPermissionsAndRunCommandAsync(() => { return Camera.requestCameraPermissionsAsync(); }); await TestUtils.acceptPermissionsAndRunCommandAsync(() => { return Camera.requestMicrophonePermissionsAsync(); }); originalTimeout = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout * 3; }); t.afterAll(() => { t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); t.beforeEach(async () => { const { status } = await Camera.getCameraPermissionsAsync(); t.expect(status).toEqual('granted'); }); t.afterEach(async () => { instance = null; await cleanupPortal(); }); t.describe('Camera.getCameraPermissionsAsync', () => { t.it('is granted', async () => { const { status } = await Camera.getCameraPermissionsAsync(); t.expect(status).toEqual('granted'); }); }); t.describe('Camera.getMicrophonePermissionsAsync', () => { t.it('is granted', async () => { const { status } = await Camera.getMicrophonePermissionsAsync(); t.expect(status).toEqual('granted'); }); }); if (Platform.OS === 'android') { t.describe('Camera.getSupportedRatiosAsync', () => { t.it('returns an array of strings', async () => { await mountAndWaitFor(); const ratios = await instance.getSupportedRatiosAsync(); t.expect(ratios instanceof Array).toBe(true); t.expect(ratios.length).toBeGreaterThan(0); }); }); } // NOTE(2020-06-03): These tests are very flaky on Android so we're disabling them for now if (Platform.OS !== 'android') { t.describe('Camera.takePictureAsync', () => { t.it('returns a local URI', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync(); t.expect(picture).toBeDefined(); t.expect(picture.uri).toMatch(/^file:\/\//); }); t.it('returns `width` and `height` of the image', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync(); t.expect(picture).toBeDefined(); t.expect(picture.width).toBeDefined(); t.expect(picture.height).toBeDefined(); }); t.it('returns EXIF only if requested', async () => { await mountAndWaitFor(); let picture = await instance.takePictureAsync({ exif: false }); t.expect(picture).toBeDefined(); t.expect(picture.exif).not.toBeDefined(); picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); }); t.it('adds additional EXIF only if requested', async () => { await mountAndWaitFor(); const additionalExif = { GPSLatitude: 30.82123, GPSLongitude: 150.25582, GPSAltitude: 80.808, }; let picture = await instance.takePictureAsync({ exif: false, additionalExif }); t.expect(picture).toBeDefined(); t.expect(picture.exif).not.toBeDefined(); picture = await instance.takePictureAsync({ exif: true, additionalExif }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.GPSLatitude).toBe(additionalExif.GPSLatitude); t.expect(picture.exif.GPSLongitude).toBe(additionalExif.GPSLongitude); t.expect(picture.exif.GPSAltitude).toBe(additionalExif.GPSAltitude); }); t.it( `returns Base64 only if requested, and not contains newline and special characters (\n or \r)`, async () => { await mountAndWaitFor(); let picture = await instance.takePictureAsync({ base64: false }); t.expect(picture).toBeDefined(); t.expect(picture.base64).not.toBeDefined(); picture = await instance.takePictureAsync({ base64: true }); t.expect(picture).toBeDefined(); t.expect(picture.base64).toBeDefined(); t.expect(picture.base64).not.toContain('\n'); t.expect(picture.base64).not.toContain('\r'); } ); t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.Flash % 2 === 0).toBe(true); }); if (Platform.OS === 'ios') { // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html // Android returns invalid values! (I've tested the code on an Android tablet // that has no flash and it returns Flash = 0, meaning that the flash did not fire, // but is present.) t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.Flash % 2 === 1).toBe(true); }); } // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.WhiteBalance).toEqual(1); }); t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.WhiteBalance).toEqual(0); }); if (Platform.OS === 'ios') { t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.LensModel).toMatch('back'); await cleanupPortal(); }); t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => { await mountAndWaitFor( ); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.LensModel).toMatch('front'); await cleanupPortal(); }); t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); await cleanupPortal(); }); t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); await cleanupPortal(); }); let smallerRatio = null; t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0); smallerRatio = picture.exif.DigitalZoomRatio; await cleanupPortal(); }); t.it( 'returns `exif.DigitalZoom`s monotonically increasing with the zoom value', async () => { await mountAndWaitFor(); const picture = await instance.takePictureAsync({ exif: true }); t.expect(picture).toBeDefined(); t.expect(picture.exif).toBeDefined(); t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio); await cleanupPortal(); } ); } }); } t.describe('Camera.recordAsync', () => { t.beforeEach(async () => { if (Platform.OS === 'ios') { await waitFor(500); } }); t.it('returns a local URI', async () => { await mountAndWaitFor(); const recordingPromise = instance.recordAsync(); await waitFor(2500); instance.stopRecording(); const response = await recordingPromise; t.expect(response).toBeDefined(); t.expect(response.uri).toMatch(/^file:\/\//); }); if (Platform.OS === 'ios') { t.it('throws for an unavailable codec', async () => { await mountAndWaitFor(); await instance .recordAsync({ codec: '123', }) .catch((error) => { t.expect(error.message).toMatch(/(?=.*codec)(?=.*is not supported)/i); }); }); t.it('returns available codecs', async () => { const codecs = await Camera.getAvailableVideoCodecsAsync(); t.expect(codecs).toBeDefined(); t.expect(codecs.length).toBeGreaterThan(0); }); } let recordedFileUri = null; t.it('stops the recording after maxDuration', async () => { await mountAndWaitFor(); const response = await instance.recordAsync({ maxDuration: 2 }); recordedFileUri = response.uri; }); t.it('the video has a duration near maxDuration', async () => { await mountAndWaitFor(