1import { Audio, Video } from 'expo-av'; 2import { Camera } from 'expo-camera'; 3import React from 'react'; 4import { Platform } from 'react-native'; 5 6import * as TestUtils from '../TestUtils'; 7import { waitFor, mountAndWaitFor as originalMountAndWaitFor, retryForStatus } from './helpers'; 8 9export const name = 'Camera'; 10const style = { width: 200, height: 200 }; 11 12export async function test(t, { setPortalChild, cleanupPortal }) { 13 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 14 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 15 16 describeWithPermissions('Camera', () => { 17 let instance = null; 18 let originalTimeout; 19 20 const refSetter = ref => { 21 instance = ref; 22 }; 23 24 const mountAndWaitFor = (child, propName = 'onCameraReady') => 25 new Promise(resolve => { 26 const response = originalMountAndWaitFor(child, propName, setPortalChild); 27 setTimeout(() => resolve(response), 1500); 28 }); 29 30 t.beforeAll(async () => { 31 await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 32 return Camera.requestPermissionsAsync(); 33 }); 34 await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 35 return Audio.requestPermissionsAsync(); 36 }); 37 38 originalTimeout = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; 39 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout * 3; 40 }); 41 42 t.afterAll(() => { 43 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; 44 }); 45 46 t.beforeEach(async () => { 47 const { status } = await Camera.getPermissionsAsync(); 48 t.expect(status).toEqual('granted'); 49 }); 50 51 t.afterEach(async () => { 52 instance = null; 53 await cleanupPortal(); 54 }); 55 56 if (Platform.OS === 'android') { 57 t.describe('Camera.getSupportedRatiosAsync', () => { 58 t.it('returns an array of strings', async () => { 59 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 60 const ratios = await instance.getSupportedRatiosAsync(); 61 t.expect(ratios instanceof Array).toBe(true); 62 t.expect(ratios.length).toBeGreaterThan(0); 63 }); 64 }); 65 } 66 67 // NOTE(2020-06-03): These tests are very flaky on Android so we're disabling them for now 68 if (Platform.OS !== 'android') { 69 t.describe('Camera.takePictureAsync', () => { 70 t.it('returns a local URI', async () => { 71 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 72 const picture = await instance.takePictureAsync(); 73 t.expect(picture).toBeDefined(); 74 t.expect(picture.uri).toMatch(/^file:\/\//); 75 }); 76 77 t.it('returns `width` and `height` of the image', async () => { 78 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 79 const picture = await instance.takePictureAsync(); 80 t.expect(picture).toBeDefined(); 81 t.expect(picture.width).toBeDefined(); 82 t.expect(picture.height).toBeDefined(); 83 }); 84 85 t.it('returns EXIF only if requested', async () => { 86 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 87 let picture = await instance.takePictureAsync({ exif: false }); 88 t.expect(picture).toBeDefined(); 89 t.expect(picture.exif).not.toBeDefined(); 90 91 picture = await instance.takePictureAsync({ exif: true }); 92 t.expect(picture).toBeDefined(); 93 t.expect(picture.exif).toBeDefined(); 94 }); 95 96 t.it( 97 `returns Base64 only if requested, and not contains newline and 98 special characters (\n or \r)`, 99 async () => { 100 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 101 let picture = await instance.takePictureAsync({ base64: false }); 102 t.expect(picture).toBeDefined(); 103 t.expect(picture.base64).not.toBeDefined(); 104 105 picture = await instance.takePictureAsync({ base64: true }); 106 t.expect(picture).toBeDefined(); 107 t.expect(picture.base64).toBeDefined(); 108 t.expect(picture.base64).not.toContain('\n'); 109 t.expect(picture.base64).not.toContain('\r'); 110 } 111 ); 112 113 t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => { 114 await mountAndWaitFor( 115 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.off} style={style} /> 116 ); 117 const picture = await instance.takePictureAsync({ exif: true }); 118 t.expect(picture).toBeDefined(); 119 t.expect(picture.exif).toBeDefined(); 120 t.expect(picture.exif.Flash % 2 === 0).toBe(true); 121 }); 122 123 if (Platform.OS === 'ios') { 124 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html 125 // Android returns invalid values! (I've tested the code on an Android tablet 126 // that has no flash and it returns Flash = 0, meaning that the flash did not fire, 127 // but is present.) 128 129 t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => { 130 await mountAndWaitFor( 131 <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.on} style={style} /> 132 ); 133 const picture = await instance.takePictureAsync({ exif: true }); 134 t.expect(picture).toBeDefined(); 135 t.expect(picture.exif).toBeDefined(); 136 t.expect(picture.exif.Flash % 2 === 1).toBe(true); 137 }); 138 } 139 140 // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html 141 142 t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => { 143 await mountAndWaitFor( 144 <Camera 145 style={style} 146 ref={refSetter} 147 whiteBalance={Camera.Constants.WhiteBalance.incandescent} 148 /> 149 ); 150 const picture = await instance.takePictureAsync({ exif: true }); 151 t.expect(picture).toBeDefined(); 152 t.expect(picture.exif).toBeDefined(); 153 t.expect(picture.exif.WhiteBalance).toEqual(1); 154 }); 155 156 t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => { 157 await mountAndWaitFor( 158 <Camera 159 style={style} 160 ref={refSetter} 161 whiteBalance={Camera.Constants.WhiteBalance.auto} 162 /> 163 ); 164 const picture = await instance.takePictureAsync({ exif: true }); 165 t.expect(picture).toBeDefined(); 166 t.expect(picture.exif).toBeDefined(); 167 t.expect(picture.exif.WhiteBalance).toEqual(0); 168 }); 169 170 if (Platform.OS === 'ios') { 171 t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => { 172 await mountAndWaitFor( 173 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.back} /> 174 ); 175 const picture = await instance.takePictureAsync({ exif: true }); 176 t.expect(picture).toBeDefined(); 177 t.expect(picture.exif).toBeDefined(); 178 t.expect(picture.exif.LensModel).toMatch('back'); 179 }); 180 181 t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => { 182 await mountAndWaitFor( 183 <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} /> 184 ); 185 const picture = await instance.takePictureAsync({ exif: true }); 186 t.expect(picture).toBeDefined(); 187 t.expect(picture.exif).toBeDefined(); 188 t.expect(picture.exif.LensModel).toMatch('front'); 189 }); 190 191 t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => { 192 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 193 const picture = await instance.takePictureAsync({ exif: true }); 194 t.expect(picture).toBeDefined(); 195 t.expect(picture.exif).toBeDefined(); 196 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 197 }); 198 199 t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => { 200 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0} />); 201 const picture = await instance.takePictureAsync({ exif: true }); 202 t.expect(picture).toBeDefined(); 203 t.expect(picture.exif).toBeDefined(); 204 t.expect(picture.exif.DigitalZoomRatio).toBeFalsy(); 205 }); 206 207 let smallerRatio = null; 208 209 t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => { 210 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />); 211 const picture = await instance.takePictureAsync({ exif: true }); 212 t.expect(picture).toBeDefined(); 213 t.expect(picture.exif).toBeDefined(); 214 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0); 215 smallerRatio = picture.exif.DigitalZoomRatio; 216 }); 217 218 t.it( 219 'returns `exif.DigitalZoom`s monotonically increasing with the zoom value', 220 async () => { 221 await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />); 222 const picture = await instance.takePictureAsync({ exif: true }); 223 t.expect(picture).toBeDefined(); 224 t.expect(picture.exif).toBeDefined(); 225 t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio); 226 } 227 ); 228 } 229 }); 230 } 231 232 t.describe('Camera.recordAsync', () => { 233 t.beforeEach(async () => { 234 if (Platform.OS === 'ios') { 235 await waitFor(500); 236 } 237 }); 238 239 t.it('returns a local URI', async () => { 240 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 241 const recordingPromise = instance.recordAsync(); 242 await waitFor(2500); 243 instance.stopRecording(); 244 const response = await recordingPromise; 245 t.expect(response).toBeDefined(); 246 t.expect(response.uri).toMatch(/^file:\/\//); 247 }); 248 249 let recordedFileUri = null; 250 251 t.it('stops the recording after maxDuration', async () => { 252 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 253 const response = await instance.recordAsync({ maxDuration: 2 }); 254 recordedFileUri = response.uri; 255 }); 256 257 t.it('the video has a duration near maxDuration', async () => { 258 await mountAndWaitFor( 259 <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />, 260 'onLoad' 261 ); 262 await retryForStatus(instance, { isBuffering: false }); 263 const video = await instance.getStatusAsync(); 264 t.expect(video.durationMillis).toBeLessThan(2250); 265 t.expect(video.durationMillis).toBeGreaterThan(1750); 266 }); 267 268 // Test for the fix to: https://github.com/expo/expo/issues/1976 269 const testFrontCameraRecording = async camera => { 270 await mountAndWaitFor(camera); 271 const response = await instance.recordAsync({ maxDuration: 2 }); 272 273 await mountAndWaitFor( 274 <Video style={style} source={{ uri: response.uri }} ref={refSetter} />, 275 'onLoad' 276 ); 277 await retryForStatus(instance, { isBuffering: false }); 278 const video = await instance.getStatusAsync(); 279 280 t.expect(video.durationMillis).toBeLessThan(2250); 281 t.expect(video.durationMillis).toBeGreaterThan(1750); 282 }; 283 284 t.it('records using the front camera', async () => { 285 await testFrontCameraRecording( 286 <Camera 287 ref={refSetter} 288 style={style} 289 type={Camera.Constants.Type.front} 290 useCamera2Api={false} 291 /> 292 ); 293 }); 294 295 if (Platform.OS === 'android') { 296 t.it('records using the front camera and Camera2 API', async () => { 297 await testFrontCameraRecording( 298 <Camera 299 ref={refSetter} 300 style={style} 301 type={Camera.Constants.Type.front} 302 useCamera2Api 303 /> 304 ); 305 }); 306 } 307 308 t.it('stops the recording after maxFileSize', async () => { 309 await mountAndWaitFor(<Camera ref={refSetter} style={style} />); 310 await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB 311 }); 312 313 t.describe('can record consecutive clips', () => { 314 let defaultTimeoutInterval = null; 315 t.beforeAll(() => { 316 defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL; 317 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2; 318 }); 319 320 t.afterAll(() => { 321 t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval; 322 }); 323 324 t.it('started/stopped manually', async () => { 325 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 326 327 const recordFor = duration => 328 new Promise(async (resolve, reject) => { 329 const recordingPromise = instance.recordAsync(); 330 await waitFor(duration); 331 instance.stopRecording(); 332 try { 333 const recordedVideo = await recordingPromise; 334 t.expect(recordedVideo).toBeDefined(); 335 t.expect(recordedVideo.uri).toBeDefined(); 336 resolve(); 337 } catch (error) { 338 reject(error); 339 } 340 }); 341 342 await recordFor(1000); 343 await waitFor(1000); 344 await recordFor(1000); 345 }); 346 347 t.it('started/stopped automatically', async () => { 348 await mountAndWaitFor(<Camera style={style} ref={refSetter} />); 349 350 const recordFor = duration => 351 new Promise(async (resolve, reject) => { 352 try { 353 const response = await instance.recordAsync({ maxDuration: duration / 1000 }); 354 resolve(response); 355 } catch (error) { 356 reject(error); 357 } 358 }); 359 360 await recordFor(1000); 361 await recordFor(1000); 362 }); 363 }); 364 }); 365 }); 366} 367