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