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