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