xref: /expo/apps/test-suite/tests/Camera.js (revision cd4bd26b)
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            await cleanupPortal();
180          });
181
182          t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => {
183            await mountAndWaitFor(
184              <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} />
185            );
186            const picture = await instance.takePictureAsync({ exif: true });
187            t.expect(picture).toBeDefined();
188            t.expect(picture.exif).toBeDefined();
189            t.expect(picture.exif.LensModel).toMatch('front');
190            await cleanupPortal();
191          });
192
193          t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => {
194            await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
195            const picture = await instance.takePictureAsync({ exif: true });
196            t.expect(picture).toBeDefined();
197            t.expect(picture.exif).toBeDefined();
198            t.expect(picture.exif.DigitalZoomRatio).toBeFalsy();
199            await cleanupPortal();
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            await cleanupPortal();
209          });
210
211          let smallerRatio = null;
212
213          t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => {
214            await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />);
215            const picture = await instance.takePictureAsync({ exif: true });
216            t.expect(picture).toBeDefined();
217            t.expect(picture.exif).toBeDefined();
218            t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0);
219            smallerRatio = picture.exif.DigitalZoomRatio;
220            await cleanupPortal();
221          });
222
223          t.it(
224            'returns `exif.DigitalZoom`s monotonically increasing with the zoom value',
225            async () => {
226              await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />);
227              const picture = await instance.takePictureAsync({ exif: true });
228              t.expect(picture).toBeDefined();
229              t.expect(picture.exif).toBeDefined();
230              t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio);
231              await cleanupPortal();
232            }
233          );
234        }
235      });
236    }
237
238    t.describe('Camera.recordAsync', () => {
239      t.beforeEach(async () => {
240        if (Platform.OS === 'ios') {
241          await waitFor(500);
242        }
243      });
244
245      t.it('returns a local URI', async () => {
246        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
247        const recordingPromise = instance.recordAsync();
248        await waitFor(2500);
249        instance.stopRecording();
250        const response = await recordingPromise;
251        t.expect(response).toBeDefined();
252        t.expect(response.uri).toMatch(/^file:\/\//);
253      });
254
255      if (Platform.OS === 'ios') {
256        t.it('throws for an unavailable codec', async () => {
257          await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
258
259          await instance
260            .recordAsync({
261              codec: '123',
262            })
263            .catch(error => {
264              t.expect(error.message).toMatch(/(?=.*codec)(?=.*is not supported)/i);
265            });
266        });
267
268        t.it('returns available codecs', async () => {
269          const codecs = await Camera.getAvailableVideoCodecsAsync();
270          t.expect(codecs).toBeDefined();
271          t.expect(codecs.length).toBeGreaterThan(0);
272        });
273      }
274
275      let recordedFileUri = null;
276
277      t.it('stops the recording after maxDuration', async () => {
278        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
279        const response = await instance.recordAsync({ maxDuration: 2 });
280        recordedFileUri = response.uri;
281      });
282
283      t.it('the video has a duration near maxDuration', async () => {
284        await mountAndWaitFor(
285          <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />,
286          'onLoad'
287        );
288        await retryForStatus(instance, { isBuffering: false });
289        const video = await instance.getStatusAsync();
290        t.expect(video.durationMillis).toBeLessThan(2250);
291        t.expect(video.durationMillis).toBeGreaterThan(1750);
292      });
293
294      // Test for the fix to: https://github.com/expo/expo/issues/1976
295      const testFrontCameraRecording = async camera => {
296        await mountAndWaitFor(camera);
297        const response = await instance.recordAsync({ maxDuration: 2 });
298
299        await mountAndWaitFor(
300          <Video style={style} source={{ uri: response.uri }} ref={refSetter} />,
301          'onLoad'
302        );
303        await retryForStatus(instance, { isBuffering: false });
304        const video = await instance.getStatusAsync();
305
306        t.expect(video.durationMillis).toBeLessThan(2250);
307        t.expect(video.durationMillis).toBeGreaterThan(1750);
308      };
309
310      t.it('records using the front camera', async () => {
311        await testFrontCameraRecording(
312          <Camera
313            ref={refSetter}
314            style={style}
315            type={Camera.Constants.Type.front}
316            useCamera2Api={false}
317          />
318        );
319      });
320
321      if (Platform.OS === 'android') {
322        t.it('records using the front camera and Camera2 API', async () => {
323          await testFrontCameraRecording(
324            <Camera
325              ref={refSetter}
326              style={style}
327              type={Camera.Constants.Type.front}
328              useCamera2Api
329            />
330          );
331        });
332      }
333
334      t.it('stops the recording after maxFileSize', async () => {
335        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
336        await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB
337      });
338
339      t.describe('can record consecutive clips', () => {
340        let defaultTimeoutInterval = null;
341        t.beforeAll(() => {
342          defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
343          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2;
344        });
345
346        t.afterAll(() => {
347          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval;
348        });
349
350        t.it('started/stopped manually', async () => {
351          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
352
353          const recordFor = duration =>
354            new Promise(async (resolve, reject) => {
355              const recordingPromise = instance.recordAsync();
356              await waitFor(duration);
357              instance.stopRecording();
358              try {
359                const recordedVideo = await recordingPromise;
360                t.expect(recordedVideo).toBeDefined();
361                t.expect(recordedVideo.uri).toBeDefined();
362                resolve();
363              } catch (error) {
364                reject(error);
365              }
366            });
367
368          await recordFor(1000);
369          await waitFor(1000);
370          await recordFor(1000);
371        });
372
373        t.it('started/stopped automatically', async () => {
374          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
375
376          const recordFor = duration =>
377            new Promise(async (resolve, reject) => {
378              try {
379                const response = await instance.recordAsync({ maxDuration: duration / 1000 });
380                resolve(response);
381              } catch (error) {
382                reject(error);
383              }
384            });
385
386          await recordFor(1000);
387          await recordFor(1000);
388        });
389      });
390    });
391  });
392}
393