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