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