xref: /expo/apps/test-suite/tests/Camera.js (revision 51f41fa4)
1import { 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 =
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(
112          `returns Base64 only if requested, and not contains newline and
113          special characters (\n or \r)`,
114          async () => {
115            await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
116            let picture = await instance.takePictureAsync({ base64: false });
117            t.expect(picture).toBeDefined();
118            t.expect(picture.base64).not.toBeDefined();
119
120            picture = await instance.takePictureAsync({ base64: true });
121            t.expect(picture).toBeDefined();
122            t.expect(picture.base64).toBeDefined();
123            t.expect(picture.base64).not.toContain('\n');
124            t.expect(picture.base64).not.toContain('\r');
125          }
126        );
127
128        t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => {
129          await mountAndWaitFor(
130            <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.off} style={style} />
131          );
132          const picture = await instance.takePictureAsync({ exif: true });
133          t.expect(picture).toBeDefined();
134          t.expect(picture.exif).toBeDefined();
135          t.expect(picture.exif.Flash % 2 === 0).toBe(true);
136        });
137
138        if (Platform.OS === 'ios') {
139          // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html
140          // Android returns invalid values! (I've tested the code on an Android tablet
141          // that has no flash and it returns Flash = 0, meaning that the flash did not fire,
142          // but is present.)
143
144          t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => {
145            await mountAndWaitFor(
146              <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.on} style={style} />
147            );
148            const picture = await instance.takePictureAsync({ exif: true });
149            t.expect(picture).toBeDefined();
150            t.expect(picture.exif).toBeDefined();
151            t.expect(picture.exif.Flash % 2 === 1).toBe(true);
152          });
153        }
154
155        // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html
156
157        t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => {
158          await mountAndWaitFor(
159            <Camera
160              style={style}
161              ref={refSetter}
162              whiteBalance={Camera.Constants.WhiteBalance.incandescent}
163            />
164          );
165          const picture = await instance.takePictureAsync({ exif: true });
166          t.expect(picture).toBeDefined();
167          t.expect(picture.exif).toBeDefined();
168          t.expect(picture.exif.WhiteBalance).toEqual(1);
169        });
170
171        t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => {
172          await mountAndWaitFor(
173            <Camera
174              style={style}
175              ref={refSetter}
176              whiteBalance={Camera.Constants.WhiteBalance.auto}
177            />
178          );
179          const picture = await instance.takePictureAsync({ exif: true });
180          t.expect(picture).toBeDefined();
181          t.expect(picture.exif).toBeDefined();
182          t.expect(picture.exif.WhiteBalance).toEqual(0);
183        });
184
185        if (Platform.OS === 'ios') {
186          t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => {
187            await mountAndWaitFor(
188              <Camera style={style} ref={refSetter} type={Camera.Constants.Type.back} />
189            );
190            const picture = await instance.takePictureAsync({ exif: true });
191            t.expect(picture).toBeDefined();
192            t.expect(picture.exif).toBeDefined();
193            t.expect(picture.exif.LensModel).toMatch('back');
194            await cleanupPortal();
195          });
196
197          t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => {
198            await mountAndWaitFor(
199              <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} />
200            );
201            const picture = await instance.takePictureAsync({ exif: true });
202            t.expect(picture).toBeDefined();
203            t.expect(picture.exif).toBeDefined();
204            t.expect(picture.exif.LensModel).toMatch('front');
205            await cleanupPortal();
206          });
207
208          t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => {
209            await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
210            const picture = await instance.takePictureAsync({ exif: true });
211            t.expect(picture).toBeDefined();
212            t.expect(picture.exif).toBeDefined();
213            t.expect(picture.exif.DigitalZoomRatio).toBeFalsy();
214            await cleanupPortal();
215          });
216
217          t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => {
218            await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0} />);
219            const picture = await instance.takePictureAsync({ exif: true });
220            t.expect(picture).toBeDefined();
221            t.expect(picture.exif).toBeDefined();
222            t.expect(picture.exif.DigitalZoomRatio).toBeFalsy();
223            await cleanupPortal();
224          });
225
226          let smallerRatio = null;
227
228          t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => {
229            await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />);
230            const picture = await instance.takePictureAsync({ exif: true });
231            t.expect(picture).toBeDefined();
232            t.expect(picture.exif).toBeDefined();
233            t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0);
234            smallerRatio = picture.exif.DigitalZoomRatio;
235            await cleanupPortal();
236          });
237
238          t.it(
239            'returns `exif.DigitalZoom`s monotonically increasing with the zoom value',
240            async () => {
241              await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />);
242              const picture = await instance.takePictureAsync({ exif: true });
243              t.expect(picture).toBeDefined();
244              t.expect(picture.exif).toBeDefined();
245              t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio);
246              await cleanupPortal();
247            }
248          );
249        }
250      });
251    }
252
253    t.describe('Camera.recordAsync', () => {
254      t.beforeEach(async () => {
255        if (Platform.OS === 'ios') {
256          await waitFor(500);
257        }
258      });
259
260      t.it('returns a local URI', async () => {
261        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
262        const recordingPromise = instance.recordAsync();
263        await waitFor(2500);
264        instance.stopRecording();
265        const response = await recordingPromise;
266        t.expect(response).toBeDefined();
267        t.expect(response.uri).toMatch(/^file:\/\//);
268      });
269
270      if (Platform.OS === 'ios') {
271        t.it('throws for an unavailable codec', async () => {
272          await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
273
274          await instance
275            .recordAsync({
276              codec: '123',
277            })
278            .catch((error) => {
279              t.expect(error.message).toMatch(/(?=.*codec)(?=.*is not supported)/i);
280            });
281        });
282
283        t.it('returns available codecs', async () => {
284          const codecs = await Camera.getAvailableVideoCodecsAsync();
285          t.expect(codecs).toBeDefined();
286          t.expect(codecs.length).toBeGreaterThan(0);
287        });
288      }
289
290      let recordedFileUri = null;
291
292      t.it('stops the recording after maxDuration', async () => {
293        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
294        const response = await instance.recordAsync({ maxDuration: 2 });
295        recordedFileUri = response.uri;
296      });
297
298      t.it('the video has a duration near maxDuration', async () => {
299        await mountAndWaitFor(
300          <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />,
301          'onLoad'
302        );
303        await retryForStatus(instance, { isBuffering: false });
304        const video = await instance.getStatusAsync();
305        t.expect(video.durationMillis).toBeLessThan(2250);
306        t.expect(video.durationMillis).toBeGreaterThan(1750);
307      });
308
309      // Test for the fix to: https://github.com/expo/expo/issues/1976
310      const testFrontCameraRecording = async (camera) => {
311        await mountAndWaitFor(camera);
312        const response = await instance.recordAsync({ maxDuration: 2 });
313
314        await mountAndWaitFor(
315          <Video style={style} source={{ uri: response.uri }} ref={refSetter} />,
316          'onLoad'
317        );
318        await retryForStatus(instance, { isBuffering: false });
319        const video = await instance.getStatusAsync();
320
321        t.expect(video.durationMillis).toBeLessThan(2250);
322        t.expect(video.durationMillis).toBeGreaterThan(1750);
323      };
324
325      t.it('records using the front camera', async () => {
326        await testFrontCameraRecording(
327          <Camera
328            ref={refSetter}
329            style={style}
330            type={Camera.Constants.Type.front}
331            useCamera2Api={false}
332          />
333        );
334      });
335
336      if (Platform.OS === 'android') {
337        t.it('records using the front camera and Camera2 API', async () => {
338          await testFrontCameraRecording(
339            <Camera
340              ref={refSetter}
341              style={style}
342              type={Camera.Constants.Type.front}
343              useCamera2Api
344            />
345          );
346        });
347      }
348
349      t.it('stops the recording after maxFileSize', async () => {
350        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
351        await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB
352      });
353
354      t.describe('can record consecutive clips', () => {
355        let defaultTimeoutInterval = null;
356        t.beforeAll(() => {
357          defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
358          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2;
359        });
360
361        t.afterAll(() => {
362          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval;
363        });
364
365        t.it('started/stopped manually', async () => {
366          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
367
368          const recordFor = (duration) =>
369            new Promise(async (resolve, reject) => {
370              const recordingPromise = instance.recordAsync();
371              await waitFor(duration);
372              instance.stopRecording();
373              try {
374                const recordedVideo = await recordingPromise;
375                t.expect(recordedVideo).toBeDefined();
376                t.expect(recordedVideo.uri).toBeDefined();
377                resolve();
378              } catch (error) {
379                reject(error);
380              }
381            });
382
383          await recordFor(1000);
384          await waitFor(1000);
385          await recordFor(1000);
386        });
387
388        t.it('started/stopped automatically', async () => {
389          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
390
391          const recordFor = (duration) =>
392            new Promise(async (resolve, reject) => {
393              try {
394                const response = await instance.recordAsync({ maxDuration: duration / 1000 });
395                resolve(response);
396              } catch (error) {
397                reject(error);
398              }
399            });
400
401          await recordFor(1000);
402          await recordFor(1000);
403        });
404      });
405    });
406  });
407}
408