xref: /expo/apps/test-suite/tests/Camera.js (revision cb0e1ce3)
1'use strict';
2
3import React from 'react';
4import { Platform } from 'react-native';
5import { Video } from 'expo-av';
6import * as Permissions from 'expo-permissions';
7import { Camera } from 'expo-camera';
8import * as TestUtils from '../TestUtils';
9
10import { waitFor, mountAndWaitFor as originalMountAndWaitFor, retryForStatus } from './helpers';
11
12export const name = 'Camera';
13const style = { width: 200, height: 200 };
14
15export async function test(t, { setPortalChild, cleanupPortal }) {
16  const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync();
17  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe;
18
19  describeWithPermissions('Camera', () => {
20    let instance = null;
21    let originalTimeout;
22
23    const refSetter = ref => {
24      instance = ref;
25    };
26
27    const mountAndWaitFor = (child, propName = 'onCameraReady') =>
28      new Promise(resolve => {
29        const response = originalMountAndWaitFor(child, propName, setPortalChild);
30        setTimeout(() => resolve(response), 1500);
31      });
32
33    t.beforeAll(async () => {
34      await TestUtils.acceptPermissionsAndRunCommandAsync(() => {
35        return Permissions.askAsync(Permissions.CAMERA);
36      });
37      await TestUtils.acceptPermissionsAndRunCommandAsync(() => {
38        return Permissions.askAsync(Permissions.AUDIO_RECORDING);
39      });
40
41      originalTimeout = t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
42      t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout * 3;
43    });
44
45    t.afterAll(() => {
46      t.jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
47    });
48
49    t.beforeEach(async () => {
50      const { status } = await Permissions.getAsync(Permissions.CAMERA);
51      t.expect(status).toEqual('granted');
52    });
53
54    t.afterEach(async () => {
55      instance = null;
56      await cleanupPortal();
57    });
58
59    if (Platform.OS === 'android') {
60      t.describe('Camera.getSupportedRatiosAsync', () => {
61        t.it('returns an array of strings', async () => {
62          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
63          const ratios = await instance.getSupportedRatiosAsync();
64          t.expect(ratios instanceof Array).toBe(true);
65          t.expect(ratios.length).toBeGreaterThan(0);
66        });
67      });
68    }
69
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        let 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('returns Base64 only if requested', async () => {
98        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
99        let picture = await instance.takePictureAsync({ base64: false });
100        t.expect(picture).toBeDefined();
101        t.expect(picture.base64).not.toBeDefined();
102
103        picture = await instance.takePictureAsync({ base64: true });
104        t.expect(picture).toBeDefined();
105        t.expect(picture.base64).toBeDefined();
106      });
107
108      t.it('returns proper `exif.Flash % 2 = 0` if the flash is off', async () => {
109        await mountAndWaitFor(
110          <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.off} style={style} />
111        );
112        let picture = await instance.takePictureAsync({ exif: true });
113        t.expect(picture).toBeDefined();
114        t.expect(picture.exif).toBeDefined();
115        t.expect(picture.exif.Flash % 2 === 0).toBe(true);
116      });
117
118      if (Platform.OS === 'ios') {
119        // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/flash.html
120        // Android returns invalid values! (I've tested the code on an Android tablet
121        // that has no flash and it returns Flash = 0, meaning that the flash did not fire,
122        // but is present.)
123
124        t.it('returns proper `exif.Flash % 2 = 1` if the flash is on', async () => {
125          await mountAndWaitFor(
126            <Camera ref={refSetter} flashMode={Camera.Constants.FlashMode.on} style={style} />
127          );
128          let picture = await instance.takePictureAsync({ exif: true });
129          t.expect(picture).toBeDefined();
130          t.expect(picture.exif).toBeDefined();
131          t.expect(picture.exif.Flash % 2 === 1).toBe(true);
132        });
133      }
134
135      // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/whitebalance.html
136
137      t.it('returns `exif.WhiteBalance = 1` if white balance is manually set', async () => {
138        await mountAndWaitFor(
139          <Camera
140            style={style}
141            ref={refSetter}
142            whiteBalance={Camera.Constants.WhiteBalance.incandescent}
143          />
144        );
145        let picture = await instance.takePictureAsync({ exif: true });
146        t.expect(picture).toBeDefined();
147        t.expect(picture.exif).toBeDefined();
148        t.expect(picture.exif.WhiteBalance).toEqual(1);
149      });
150
151      t.it('returns `exif.WhiteBalance = 0` if white balance is set to auto', async () => {
152        await mountAndWaitFor(
153          <Camera style={style} ref={refSetter} whiteBalance={Camera.Constants.WhiteBalance.auto} />
154        );
155        let picture = await instance.takePictureAsync({ exif: true });
156        t.expect(picture).toBeDefined();
157        t.expect(picture.exif).toBeDefined();
158        t.expect(picture.exif.WhiteBalance).toEqual(0);
159      });
160
161      if (Platform.OS === 'ios') {
162        t.it('returns `exif.LensModel ~= back` if camera type is set to back', async () => {
163          await mountAndWaitFor(
164            <Camera style={style} ref={refSetter} type={Camera.Constants.Type.back} />
165          );
166          let picture = await instance.takePictureAsync({ exif: true });
167          t.expect(picture).toBeDefined();
168          t.expect(picture.exif).toBeDefined();
169          t.expect(picture.exif.LensModel).toMatch('back');
170        });
171
172        t.it('returns `exif.LensModel ~= front` if camera type is set to front', async () => {
173          await mountAndWaitFor(
174            <Camera style={style} ref={refSetter} type={Camera.Constants.Type.front} />
175          );
176          let picture = await instance.takePictureAsync({ exif: true });
177          t.expect(picture).toBeDefined();
178          t.expect(picture.exif).toBeDefined();
179          t.expect(picture.exif.LensModel).toMatch('front');
180        });
181
182        t.it('returns `exif.DigitalZoom ~= false` if zoom is not set', async () => {
183          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
184          let picture = await instance.takePictureAsync({ exif: true });
185          t.expect(picture).toBeDefined();
186          t.expect(picture.exif).toBeDefined();
187          t.expect(picture.exif.DigitalZoomRatio).toBeFalsy();
188        });
189
190        t.it('returns `exif.DigitalZoom ~= false` if zoom is set to 0', async () => {
191          await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0} />);
192          let picture = await instance.takePictureAsync({ exif: true });
193          t.expect(picture).toBeDefined();
194          t.expect(picture.exif).toBeDefined();
195          t.expect(picture.exif.DigitalZoomRatio).toBeFalsy();
196        });
197
198        let smallerRatio = null;
199
200        t.it('returns `exif.DigitalZoom > 0` if zoom is set', async () => {
201          await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={0.5} />);
202          let picture = await instance.takePictureAsync({ exif: true });
203          t.expect(picture).toBeDefined();
204          t.expect(picture.exif).toBeDefined();
205          t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(0);
206          smallerRatio = picture.exif.DigitalZoomRatio;
207        });
208
209        t.it(
210          'returns `exif.DigitalZoom`s monotonically increasing with the zoom value',
211          async () => {
212            await mountAndWaitFor(<Camera style={style} ref={refSetter} zoom={1} />);
213            let picture = await instance.takePictureAsync({ exif: true });
214            t.expect(picture).toBeDefined();
215            t.expect(picture.exif).toBeDefined();
216            t.expect(picture.exif.DigitalZoomRatio).toBeGreaterThan(smallerRatio);
217          }
218        );
219      }
220    });
221
222    t.describe('Camera.recordAsync', () => {
223      t.beforeEach(async () => {
224        if (Platform.OS === 'ios') {
225          await waitFor(500);
226        }
227      });
228
229      t.it('returns a local URI', async () => {
230        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
231        const recordingPromise = instance.recordAsync();
232        await waitFor(2500);
233        instance.stopRecording();
234        const response = await recordingPromise;
235        t.expect(response).toBeDefined();
236        t.expect(response.uri).toMatch(/^file:\/\//);
237      });
238
239      let recordedFileUri = null;
240
241      t.it('stops the recording after maxDuration', async () => {
242        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
243        const response = await instance.recordAsync({ maxDuration: 2 });
244        recordedFileUri = response.uri;
245      });
246
247      t.it('the video has a duration near maxDuration', async () => {
248        await mountAndWaitFor(
249          <Video style={style} source={{ uri: recordedFileUri }} ref={refSetter} />,
250          'onLoad'
251        );
252        await retryForStatus(instance, { isBuffering: false });
253        const video = await instance.getStatusAsync();
254        t.expect(video.durationMillis).toBeLessThan(2250);
255        t.expect(video.durationMillis).toBeGreaterThan(1750);
256      });
257
258      // Test for the fix to: https://github.com/expo/expo/issues/1976
259      const testFrontCameraRecording = async camera => {
260        await mountAndWaitFor(camera);
261        const response = await instance.recordAsync({ maxDuration: 2 });
262
263        await mountAndWaitFor(
264          <Video style={style} source={{ uri: response.uri }} ref={refSetter} />,
265          'onLoad'
266        );
267        await retryForStatus(instance, { isBuffering: false });
268        const video = await instance.getStatusAsync();
269
270        t.expect(video.durationMillis).toBeLessThan(2250);
271        t.expect(video.durationMillis).toBeGreaterThan(1750);
272      };
273
274      t.it('records using the front camera', async () => {
275        await testFrontCameraRecording(
276          <Camera
277            ref={refSetter}
278            style={style}
279            type={Camera.Constants.Type.front}
280            useCamera2Api={false}
281          />
282        );
283      });
284
285      if (Platform.OS === 'android') {
286        t.it('records using the front camera and Camera2 API', async () => {
287          await testFrontCameraRecording(
288            <Camera
289              ref={refSetter}
290              style={style}
291              type={Camera.Constants.Type.front}
292              useCamera2Api
293            />
294          );
295        });
296      }
297
298      t.it('stops the recording after maxFileSize', async () => {
299        await mountAndWaitFor(<Camera ref={refSetter} style={style} />);
300        await instance.recordAsync({ maxFileSize: 256 * 1024 }); // 256 KiB
301      });
302
303      t.describe('can record consecutive clips', () => {
304        let defaultTimeoutInterval = null;
305        t.beforeAll(() => {
306          defaultTimeoutInterval = t.jasmine.DEFAULT_TIMEOUT_INTERVAL;
307          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval * 2;
308        });
309
310        t.afterAll(() => {
311          t.jasmine.DEFAULT_TIMEOUT_INTERVAL = defaultTimeoutInterval;
312        });
313
314        t.it('started/stopped manually', async () => {
315          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
316
317          const recordFor = duration =>
318            new Promise(async (resolve, reject) => {
319              const recordingPromise = instance.recordAsync();
320              await waitFor(duration);
321              instance.stopRecording();
322              try {
323                const recordedVideo = await recordingPromise;
324                t.expect(recordedVideo).toBeDefined();
325                t.expect(recordedVideo.uri).toBeDefined();
326                resolve();
327              } catch (error) {
328                reject(error);
329              }
330            });
331
332          await recordFor(1000);
333          await waitFor(1000);
334          await recordFor(1000);
335        });
336
337        t.it('started/stopped automatically', async () => {
338          await mountAndWaitFor(<Camera style={style} ref={refSetter} />);
339
340          const recordFor = duration =>
341            new Promise(async (resolve, reject) => {
342              try {
343                const response = await instance.recordAsync({ maxDuration: duration / 1000 });
344                resolve(response);
345              } catch (error) {
346                reject(error);
347              }
348            });
349
350          await recordFor(1000);
351          await recordFor(1000);
352        });
353      });
354    });
355  });
356}
357