// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math'; import 'package:async/async.dart'; import 'package:camera_avfoundation/src/avfoundation_camera.dart'; import 'package:camera_avfoundation/src/messages.g.dart'; import 'package:camera_avfoundation/src/utils.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'avfoundation_camera_test.mocks.dart'; @GenerateNiceMocks(>[MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('registers instance', () async { AVFoundationCamera.registerWith(); expect(CameraPlatform.instance, isA()); }); group('Creation, Initialization & Disposal Tests', () { test('Should send creation data and receive back a camera id', () async { // Arrange final mockApi = MockCameraApi(); when(mockApi.create(any, any)).thenAnswer((_) async => 1); final camera = AVFoundationCamera(api: mockApi); const cameraName = 'Test'; // Act final int cameraId = await camera.createCamera( const CameraDescription( name: cameraName, lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); // Assert final VerificationResult verification = verify(mockApi.create(captureAny, captureAny)); expect(verification.captured[0], cameraName); final settings = verification.captured[1] as PlatformMediaSettings?; expect(settings, isNotNull); expect(settings?.resolutionPreset, PlatformResolutionPreset.high); expect(cameraId, 1); }); test( 'Should send creation data and receive back a camera id using createCameraWithSettings', () async { // Arrange final mockApi = MockCameraApi(); when(mockApi.create(any, any)).thenAnswer((_) async => 1); final camera = AVFoundationCamera(api: mockApi); const cameraName = 'Test'; const fps = 15; const videoBitrate = 200000; const audioBitrate = 32000; // Act final int cameraId = await camera.createCameraWithSettings( const CameraDescription( name: cameraName, lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), const MediaSettings( resolutionPreset: ResolutionPreset.low, fps: fps, videoBitrate: videoBitrate, audioBitrate: audioBitrate, enableAudio: true, ), ); // Assert final VerificationResult verification = verify(mockApi.create(captureAny, captureAny)); expect(verification.captured[0], cameraName); final settings = verification.captured[1] as PlatformMediaSettings?; expect(settings, isNotNull); expect(settings?.resolutionPreset, PlatformResolutionPreset.low); expect(settings?.framesPerSecond, fps); expect(settings?.videoBitrate, videoBitrate); expect(settings?.audioBitrate, audioBitrate); expect(settings?.enableAudio, true); expect(cameraId, 1); }, ); test('Should throw CameraException when create throws a PlatformException', () { // Arrange const exceptionCode = 'TESTING_ERROR_CODE'; const exceptionMessage = 'Mock error message used during testing.'; final mockApi = MockCameraApi(); when(mockApi.create(any, any)).thenAnswer((_) async { throw PlatformException(code: exceptionCode, message: exceptionMessage); }); final camera = AVFoundationCamera(api: mockApi); // Act expect( () => camera.createCamera( const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ), throwsA( isA() .having((CameraException e) => e.code, 'code', exceptionCode) .having((CameraException e) => e.description, 'description', exceptionMessage), ), ); }); test('Should throw CameraException when initialize throws a PlatformException', () { // Arrange const exceptionCode = 'TESTING_ERROR_CODE'; const exceptionMessage = 'Mock error message used during testing.'; final mockApi = MockCameraApi(); when(mockApi.initialize(any, any)).thenAnswer((_) async { throw PlatformException(code: exceptionCode, message: exceptionMessage); }); final camera = AVFoundationCamera(api: mockApi); // Act expect( () => camera.initializeCamera(0), throwsA( isA() .having((CameraException e) => e.code, 'code', 'TESTING_ERROR_CODE') .having( (CameraException e) => e.description, 'description', 'Mock error message used during testing.', ), ), ); }); test('Should send initialization data', () async { // Arrange final mockApi = MockCameraApi(); final camera = AVFoundationCamera(api: mockApi); final int cameraId = await camera.createCamera( const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); // Act final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), ); await initializeFuture; // Assert final VerificationResult verification = verify(mockApi.initialize(captureAny, captureAny)); expect(verification.captured[0], cameraId); // The default when unspecified should be bgra8888. expect(verification.captured[1], PlatformImageFormatGroup.bgra8888); }); test('Should send a disposal call on dispose', () async { // Arrange final mockApi = MockCameraApi(); final camera = AVFoundationCamera(api: mockApi); final int cameraId = await camera.createCamera( const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), ); await initializeFuture; // Act await camera.dispose(cameraId); // Assert final VerificationResult verification = verify(mockApi.dispose(captureAny)); expect(verification.captured[0], cameraId); }); }); group('Event Tests', () { late AVFoundationCamera camera; late int cameraId; setUp(() async { final mockApi = MockCameraApi(); when(mockApi.create(any, any)).thenAnswer((_) async => 1); camera = AVFoundationCamera(api: mockApi); cameraId = await camera.createCamera( const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), ); await initializeFuture; }); test('Should receive initialized event', () async { // Act final Stream eventStream = camera.onCameraInitialized(cameraId); final streamQueue = StreamQueue(eventStream); final previewSize = PlatformSize(width: 3840, height: 2160); // Emit test events final event = CameraInitializedEvent( cameraId, previewSize.width, previewSize.height, ExposureMode.auto, true, FocusMode.auto, true, ); camera.hostCameraHandlers[cameraId]!.initialized( PlatformCameraState( previewSize: previewSize, exposureMode: PlatformExposureMode.auto, focusMode: PlatformFocusMode.auto, exposurePointSupported: true, focusPointSupported: true, ), ); // Assert expect(await streamQueue.next, event); // Clean up await streamQueue.cancel(); }); test('Should receive camera error events', () async { // Act final Stream errorStream = camera.onCameraError(cameraId); final streamQueue = StreamQueue(errorStream); // Emit test events const errorMessage = 'Error Description'; final event = CameraErrorEvent(cameraId, errorMessage); camera.hostCameraHandlers[cameraId]!.error(errorMessage); camera.hostCameraHandlers[cameraId]!.error(errorMessage); camera.hostCameraHandlers[cameraId]!.error(errorMessage); // Assert expect(await streamQueue.next, event); expect(await streamQueue.next, event); expect(await streamQueue.next, event); // Clean up await streamQueue.cancel(); }); test('Should receive device orientation change events', () async { // Act final Stream eventStream = camera.onDeviceOrientationChanged(); final streamQueue = StreamQueue(eventStream); // Emit test events const event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (var i = 0; i < 3; i++) { camera.hostHandler.deviceOrientationChanged(PlatformDeviceOrientation.portraitUp); } // Assert expect(await streamQueue.next, event); expect(await streamQueue.next, event); expect(await streamQueue.next, event); // Clean up await streamQueue.cancel(); }); }); group('Function Tests', () { late MockCameraApi mockApi; late AVFoundationCamera camera; late int cameraId; setUp(() async { mockApi = MockCameraApi(); when(mockApi.create(any, any)).thenAnswer((_) async => 1); camera = AVFoundationCamera(api: mockApi); cameraId = await camera.createCamera( const CameraDescription( name: 'Test', lensDirection: CameraLensDirection.back, sensorOrientation: 0, ), ResolutionPreset.high, ); final Future initializeFuture = camera.initializeCamera(cameraId); camera.cameraEventStreamController.add( CameraInitializedEvent(cameraId, 1920, 1080, ExposureMode.auto, true, FocusMode.auto, true), ); await initializeFuture; }); test('Should fetch CameraDescription instances for available cameras', () async { final returnData = [ PlatformCameraDescription( name: 'Test 1', lensDirection: PlatformCameraLensDirection.front, lensType: PlatformCameraLensType.ultraWide, ), PlatformCameraDescription( name: 'Test 2', lensDirection: PlatformCameraLensDirection.back, lensType: PlatformCameraLensType.telephoto, ), ]; when(mockApi.getAvailableCameras()).thenAnswer((_) async => returnData); final List cameras = await camera.availableCameras(); expect(cameras.length, returnData.length); for (var i = 0; i < returnData.length; i++) { expect(cameras[i].name, returnData[i].name); expect( cameras[i].lensDirection, cameraLensDirectionFromPlatform(returnData[i].lensDirection), ); expect(cameras[i].lensType, cameraLensTypeFromPlatform(returnData[i].lensType)); // This value isn't provided by the platform, so is hard-coded to 90. expect(cameras[i].sensorOrientation, 90); } }); test('Should throw CameraException when availableCameras throws a PlatformException', () { const code = 'TESTING_ERROR_CODE'; const message = 'Mock error message used during testing.'; when( mockApi.getAvailableCameras(), ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); expect( camera.availableCameras, throwsA( isA() .having((CameraException e) => e.code, 'code', code) .having((CameraException e) => e.description, 'description', message), ), ); }); test('Should take a picture and return an XFile instance', () async { const stubPath = '/test/path.jpg'; when(mockApi.takePicture()).thenAnswer((_) async => stubPath); final XFile file = await camera.takePicture(cameraId); expect(file.path, stubPath); }); test('Should prepare for video recording', () async { await camera.prepareForVideoRecording(); verify(mockApi.prepareForVideoRecording()); }); test('Should start recording a video', () async { await camera.startVideoRecording(cameraId); verify(mockApi.startVideoRecording(any)); }); test( 'Should pass enableStream if callback is passed when starting recording a video', () async { await camera.startVideoCapturing( VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), ); verify(mockApi.startVideoRecording(true)); }, ); test('Should stop a video recording and return the file', () async { const stubPath = '/test/path.mp4'; when(mockApi.stopVideoRecording()).thenAnswer((_) async => stubPath); final XFile file = await camera.stopVideoRecording(cameraId); expect(file.path, stubPath); }); test('Should pause a video recording', () async { await camera.pauseVideoRecording(cameraId); verify(mockApi.pauseVideoRecording()); }); test('Should resume a video recording', () async { await camera.resumeVideoRecording(cameraId); verify(mockApi.resumeVideoRecording()); }); test('Should set the description while recording', () async { const camera2Description = CameraDescription( name: 'Test2', lensDirection: CameraLensDirection.front, sensorOrientation: 0, ); await camera.setDescriptionWhileRecording(camera2Description); verify(mockApi.updateDescriptionWhileRecording(camera2Description.name)); }); test('Should set the flash mode to torch', () async { await camera.setFlashMode(cameraId, FlashMode.torch); verify(mockApi.setFlashMode(PlatformFlashMode.torch)); }); test('Should set the flash mode to always', () async { await camera.setFlashMode(cameraId, FlashMode.always); verify(mockApi.setFlashMode(PlatformFlashMode.always)); }); test('Should set the flash mode to auto', () async { await camera.setFlashMode(cameraId, FlashMode.auto); verify(mockApi.setFlashMode(PlatformFlashMode.auto)); }); test('Should set the flash mode to off', () async { await camera.setFlashMode(cameraId, FlashMode.off); verify(mockApi.setFlashMode(PlatformFlashMode.off)); }); test('Should set the exposure mode to auto', () async { await camera.setExposureMode(cameraId, ExposureMode.auto); verify(mockApi.setExposureMode(PlatformExposureMode.auto)); }); test('Should set the exposure mode to locked', () async { await camera.setExposureMode(cameraId, ExposureMode.locked); verify(mockApi.setExposureMode(PlatformExposureMode.locked)); }); test('Should set the exposure point to a value', () async { const point = Point(0.4, 0.6); await camera.setExposurePoint(cameraId, point); final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny)); final passedPoint = verification.captured[0] as PlatformPoint?; expect(passedPoint?.x, point.x); expect(passedPoint?.y, point.y); }); test('Should set the exposure point to null for reset', () async { await camera.setExposurePoint(cameraId, null); final VerificationResult verification = verify(mockApi.setExposurePoint(captureAny)); final passedPoint = verification.captured[0] as PlatformPoint?; expect(passedPoint, null); }); test('Should get the min exposure offset', () async { const stubMinOffset = 2.0; when(mockApi.getMinExposureOffset()).thenAnswer((_) async => stubMinOffset); final double minExposureOffset = await camera.getMinExposureOffset(cameraId); expect(minExposureOffset, stubMinOffset); }); test('Should get the max exposure offset', () async { const stubMaxOffset = 2.0; when(mockApi.getMaxExposureOffset()).thenAnswer((_) async => stubMaxOffset); final double maxExposureOffset = await camera.getMaxExposureOffset(cameraId); expect(maxExposureOffset, stubMaxOffset); }); test('Exposure offset step size should always return zero', () async { final double stepSize = await camera.getExposureOffsetStepSize(cameraId); expect(stepSize, 0.0); }); test('Should set the exposure offset', () async { const stubOffset = 0.5; final double actualOffset = await camera.setExposureOffset(cameraId, 0.5); verify(mockApi.setExposureOffset(stubOffset)); // iOS never adjusts the offset. expect(actualOffset, stubOffset); }); test('Should set the focus mode to auto', () async { await camera.setFocusMode(cameraId, FocusMode.auto); verify(mockApi.setFocusMode(PlatformFocusMode.auto)); }); test('Should set the focus mode to locked', () async { await camera.setFocusMode(cameraId, FocusMode.locked); verify(mockApi.setFocusMode(PlatformFocusMode.locked)); }); test('Should set the focus point to a value', () async { const point = Point(0.4, 0.6); await camera.setFocusPoint(cameraId, point); final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny)); final passedPoint = verification.captured[0] as PlatformPoint?; expect(passedPoint?.x, point.x); expect(passedPoint?.y, point.y); }); test('Should set the focus point to null for reset', () async { await camera.setFocusPoint(cameraId, null); final VerificationResult verification = verify(mockApi.setFocusPoint(captureAny)); final passedPoint = verification.captured[0] as PlatformPoint?; expect(passedPoint, null); }); test('Should build a texture widget as preview widget', () async { final Widget widget = camera.buildPreview(cameraId); expect(widget is Texture, isTrue); expect((widget as Texture).textureId, cameraId); }); test('Should get the max zoom level', () async { const stubZoomLevel = 10.0; when(mockApi.getMaxZoomLevel()).thenAnswer((_) async => stubZoomLevel); final double maxZoomLevel = await camera.getMaxZoomLevel(cameraId); expect(maxZoomLevel, stubZoomLevel); }); test('Should get the min zoom level', () async { const stubZoomLevel = 10.0; when(mockApi.getMinZoomLevel()).thenAnswer((_) async => stubZoomLevel); final double minZoomLevel = await camera.getMinZoomLevel(cameraId); expect(minZoomLevel, stubZoomLevel); }); test('Should set the zoom level', () async { const zoom = 2.0; await camera.setZoomLevel(cameraId, zoom); verify(mockApi.setZoomLevel(zoom)); }); test('Should throw CameraException when illegal zoom level is supplied', () async { const code = 'ZOOM_ERROR'; const message = 'Illegal zoom error'; when( mockApi.setZoomLevel(any), ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); expect( () => camera.setZoomLevel(cameraId, -1.0), throwsA( isA() .having((CameraException e) => e.code, 'code', code) .having((CameraException e) => e.description, 'description', message), ), ); }); test('Should set video stabilization mode to off', () async { when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off), ).thenAnswer((_) async => true); await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off); verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.off)); }); test('Should set video stabilization mode to level1', () async { when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard), ).thenAnswer((_) async => true); await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1); verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.standard)); }); test('Should set video stabilization mode to cinematic', () async { when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic), ).thenAnswer((_) async => true); await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2); verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematic)); }); test('Should set video stabilization mode to cinematicExtended', () async { when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended), ).thenAnswer((_) async => true); await camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3); verify(mockApi.setVideoStabilizationMode(PlatformVideoStabilizationMode.cinematicExtended)); }); test('Should get no video stabilization mode', () async { when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false); final Iterable modes = await camera .getSupportedVideoStabilizationModes(cameraId); expect(modes, isEmpty); }); test('Should get off and standard video stabilization modes', () async { when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.off), ).thenAnswer((_) async => true); when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.standard), ).thenAnswer((_) async => true); when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematic), ).thenAnswer((_) async => false); when( mockApi.isVideoStabilizationModeSupported(PlatformVideoStabilizationMode.cinematicExtended), ).thenAnswer((_) async => false); final List modes = (await camera.getSupportedVideoStabilizationModes( cameraId, )).toList(); expect(modes, [ VideoStabilizationMode.off, VideoStabilizationMode.level1, ]); }); test('Should get all video stabilization modes', () async { when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => true); final List modes = (await camera.getSupportedVideoStabilizationModes( cameraId, )).toList(); expect(modes, [ VideoStabilizationMode.off, VideoStabilizationMode.level1, VideoStabilizationMode.level2, VideoStabilizationMode.level3, ]); }); test('Should throw ArgumentError when unavailable video stabilization mode is set', () async { when(mockApi.isVideoStabilizationModeSupported(any)).thenAnswer((_) async => false); expect( () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.off), throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), ); expect( () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level1), throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), ); expect( () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level2), throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), ); expect( () => camera.setVideoStabilizationMode(cameraId, VideoStabilizationMode.level3), throwsA(isA().having((ArgumentError e) => e.name, 'name', 'mode')), ); }); test('Should throw CameraException when illegal zoom level is supplied', () async { const code = 'ZOOM_ERROR'; const message = 'Illegal zoom error'; when( mockApi.setZoomLevel(any), ).thenAnswer((_) async => throw PlatformException(code: code, message: message)); expect( () => camera.setZoomLevel(cameraId, -1.0), throwsA( isA() .having((CameraException e) => e.code, 'code', code) .having((CameraException e) => e.description, 'description', message), ), ); }); test('Should lock the capture orientation', () async { await camera.lockCaptureOrientation(cameraId, DeviceOrientation.portraitUp); verify(mockApi.lockCaptureOrientation(PlatformDeviceOrientation.portraitUp)); }); test('Should unlock the capture orientation', () async { await camera.unlockCaptureOrientation(cameraId); verify(mockApi.unlockCaptureOrientation()); }); test('Should pause the camera preview', () async { await camera.pausePreview(cameraId); verify(mockApi.pausePreview()); }); test('Should resume the camera preview', () async { await camera.resumePreview(cameraId); verify(mockApi.resumePreview()); }); test('Should report support for image streaming', () async { expect(camera.supportsImageStreaming(), true); }); test('Should start streaming', () async { final StreamSubscription subscription = camera .onStreamedFrameAvailable(cameraId) .listen((CameraImageData imageData) {}); verify(mockApi.startImageStream()); await subscription.cancel(); }); test('Should stop streaming', () async { final StreamSubscription subscription = camera .onStreamedFrameAvailable(cameraId) .listen((CameraImageData imageData) {}); await subscription.cancel(); verify(mockApi.startImageStream()); verify(mockApi.stopImageStream()); }); test('Should set the ImageFileFormat to heif', () async { await camera.setImageFileFormat(cameraId, ImageFileFormat.heif); verify(mockApi.setImageFileFormat(PlatformImageFileFormat.heif)); }); test('Should set the ImageFileFormat to jpeg', () async { await camera.setImageFileFormat(cameraId, ImageFileFormat.jpeg); verify(mockApi.setImageFileFormat(PlatformImageFileFormat.jpeg)); }); test('Should set the image quality', () async { await camera.setJpegImageQuality(cameraId, 50); verify(mockApi.setJpegImageQuality(50)); }); }); }