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