diff --git a/.ci.yaml b/.ci.yaml index 714e25471f34..f24589b43c5c 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -73,7 +73,7 @@ platform_properties: device_type: none dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] windows_arm64: properties: @@ -217,6 +217,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 0 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_dart_unit_test_shard_2 stable recipe: packages/packages @@ -226,6 +234,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 1 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_dart_unit_test_shard_3 stable recipe: packages/packages @@ -235,6 +251,14 @@ targets: channel: stable version_file: flutter_stable.version package_sharding: "--shardIndex 2 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] # Wasm unit tests in master # TODO(stuartmorgan): Switch to testing wasm on master and stable and non-wasm @@ -321,7 +345,7 @@ targets: {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, {"dependency": "cmake", "version": "build_id:8787856497187628321"}, {"dependency": "ninja", "version": "version:1.9.0"}, - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] channel: master @@ -334,6 +358,10 @@ targets: version_file: flutter_stable.version target_file: linux_custom_package_tests.yaml # See comments on 'master' version above. + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "android_virtual_device", "version": "android_36_google_apis_x64.textpb"}, @@ -722,6 +750,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 0 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_platform_tests_shard_2 stable recipe: packages/packages @@ -731,6 +767,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 1 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] - name: Linux_web web_platform_tests_shard_3 stable recipe: packages/packages @@ -740,6 +784,14 @@ targets: version_file: flutter_stable.version channel: stable package_sharding: "--shardIndex 2 --shardCount 3" + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + ] ### Linux desktop tasks - name: Linux_desktop build_all_packages master @@ -769,7 +821,7 @@ targets: # Install Chrome as a default handler for schemes for url_launcher. dependencies: >- [ - {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} + {"dependency": "chrome_and_driver", "version": "version:145.0.7632.117"} ] - name: Linux_desktop platform_tests stable @@ -781,6 +833,10 @@ targets: target_file: linux_platform_tests.yaml channel: stable # Install Chrome as a default handler for schemes for url_launcher. + # TODO(mdebbar): Upgrade to Chrome 145 once Flutter stable includes the + # upstream flutter_tools fixes for Chrome 145 (flutter/flutter#190155 and + # flutter/flutter#190584) which pass --disable-search-engine-choice-screen + # and --disable-background-timer-throttling; see https://github.com/flutter/flutter/issues/191299 dependencies: >- [ {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart new file mode 100644 index 000000000000..6d6a0f0f0bfb --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_capture_test.dart @@ -0,0 +1,536 @@ +// 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('takePicture', () { + testWidgets('captures a picture', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedPicture = XFile('/bogus/test'); + + when(camera.takePicture()).thenAnswer((_) async => capturedPicture); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final XFile picture = await CameraPlatform.instance.takePicture(cameraId); + + verify(camera.takePicture()).called(1); + + expect(picture, equals(capturedPicture)); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when takePicture throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.takePicture()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when takePicture throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.takePicture()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('startVideoRecording', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('starts a video recording', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + verify(camera.startVideoRecording()).called(1); + }); + + testWidgets('listens to the onVideoRecordingError stream', (WidgetTester tester) async { + final videoRecordingErrorController = StreamController(); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + expect(videoRecordingErrorController.hasListener, isTrue); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws DomException', (WidgetTester tester) async { + final exception = DOMException('InvalidStateError'); + + when(camera.startVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.startVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('startVideoCapturing', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('fails if trying to stream', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), + ), + throwsA(isA()), + ); + }); + }); + + group('stopVideoRecording', () { + testWidgets('stops a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedVideo = XFile('/bogus/test'); + + when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final XFile video = await CameraPlatform.instance.stopVideoRecording(cameraId); + + verify(camera.stopVideoRecording()).called(1); + + expect(video, capturedVideo); + }); + + testWidgets('stops listening to the onVideoRecordingError stream', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final videoRecordingErrorController = StreamController(); + final capturedVideo = XFile('/bogus/test'); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + + when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + final XFile _ = await CameraPlatform.instance.stopVideoRecording(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.pauseVideoRecording()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pauseVideoRecording(cameraId); + + verify(camera.pauseVideoRecording()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.resumeVideoRecording()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumeVideoRecording(cameraId); + + verify(camera.resumeVideoRecording()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidStateError'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws CameraWebException', ( + WidgetTester tester, + ) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart new file mode 100644 index 000000000000..70a1197c5c22 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_controls_test.dart @@ -0,0 +1,600 @@ +// 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. + +// ignore_for_file: only_throw_errors + +import 'dart:js_interop'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('setFlashMode', () { + testWidgets('calls setFlashMode on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const FlashMode flashMode = FlashMode.always; + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.setFlashMode(cameraId, flashMode); + + verify(camera.setFlashMode(flashMode)).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setFlashMode throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setFlashMode throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.torch), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets('setExposureMode throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposureMode(cameraId, ExposureMode.auto), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposurePoint throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposurePoint(cameraId, const Point(0, 0)), + throwsUnimplementedError, + ); + }); + + testWidgets('getMinExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getMinExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getMaxExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getMaxExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getExposureOffsetStepSize throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposureOffset throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setExposureOffset(cameraId, 0), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusMode throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFocusMode(cameraId, FocusMode.auto), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusPoint throws UnimplementedError', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.setFocusPoint(cameraId, const Point(0, 0)), + throwsUnimplementedError, + ); + }); + + group('getMaxZoomLevel', () { + testWidgets('calls getMaxZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const maximumZoomLevel = 100.0; + + when(camera.getMaxZoomLevel()).thenReturn(maximumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(await CameraPlatform.instance.getMaxZoomLevel(cameraId), equals(maximumZoomLevel)); + + verify(camera.getMaxZoomLevel()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('getMinZoomLevel', () { + testWidgets('calls getMinZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + const minimumZoomLevel = 100.0; + + when(camera.getMinZoomLevel()).thenReturn(minimumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(await CameraPlatform.instance.getMinZoomLevel(cameraId), equals(minimumZoomLevel)); + + verify(camera.getMinZoomLevel()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('setZoomLevel', () { + testWidgets('calls setZoomLevel on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + const zoom = 100.0; + + await CameraPlatform.instance.setZoomLevel(cameraId, zoom); + + verify(camera.setZoomLevel(zoom)).called(1); + }); + + group('throws CameraException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.name), + ), + ); + }); + + testWidgets('when setZoomLevel throws PlatformException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.code), + ), + ); + }); + + testWidgets('when setZoomLevel throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pausePreview', () { + testWidgets('calls pause on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pausePreview(cameraId); + + verify(camera.pause()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pause throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.pause()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('resumePreview', () { + testWidgets('calls play on the camera', (WidgetTester tester) async { + final camera = MockCamera(); + + when(camera.play()).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumePreview(cameraId); + + verify(camera.play()).called(1); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when play throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('NotSupportedError'); + + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when play throws CameraWebException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); + + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets('buildPreview returns an HtmlElementView ' + 'with an appropriate view type', (WidgetTester tester) async { + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + CameraPlatform.instance.buildPreview(cameraId), + isA().having( + (widgets.HtmlElementView view) => view.viewType, + 'viewType', + camera.getViewType(), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart new file mode 100644 index 000000000000..3d09f2f1e98f --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_devices_test.dart @@ -0,0 +1,746 @@ +// 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('availableCameras', () { + setUp(() { + when(cameraService.getFacingModeForVideoTrack(any)).thenReturn(null); + + mockMediaDevices.enumerateDevices = () { + return Future>.value([].toJS).toJS; + }.toJS; + }); + + testWidgets('requests video permissions', (WidgetTester tester) async { + final List _ = await CameraPlatform.instance.availableCameras(); + + verify(cameraService.getMediaStreamForOptions(const CameraOptions())).called(1); + }); + + testWidgets('releases the camera stream ' + 'used to request video permissions', (WidgetTester tester) async { + final mockVideoTrack = MockMediaStreamTrack(); + final videoTrack = createJSInteropWrapper(mockVideoTrack) as MediaStreamTrack; + + var videoTrackStopped = false; + mockVideoTrack.stop = () { + videoTrackStopped = true; + }.toJS; + + when(cameraService.getMediaStreamForOptions(const CameraOptions())).thenAnswer( + (_) => Future.value( + createJSInteropWrapper(FakeMediaStream([videoTrack])) as MediaStream, + ), + ); + + final List _ = await CameraPlatform.instance.availableCameras(); + + expect(videoTrackStopped, isTrue); + }); + + testWidgets('gets a video stream ' + 'for a video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verify( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).called(1); + }); + + testWidgets('does not get a video stream ' + 'for the video input device ' + 'with an empty device id', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verifyNever( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ); + }); + + testWidgets('gets the facing mode ' + 'from the first available video track ' + 'of the video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + final List _ = await CameraPlatform.instance.availableCameras(); + + verify( + cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), + ).called(1); + }); + + testWidgets('returns appropriate camera descriptions ' + 'for multiple video devices ' + 'based on video streams', (WidgetTester tester) async { + final firstVideoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final secondVideoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('4', 'Camera 4', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + // Create a video stream for the first video device. + final firstVideoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + // Create a video stream for the second video device. + final secondVideoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + // Mock media devices to return two video input devices + // and two audio devices. + mockMediaDevices.enumerateDevices = () { + return Future>.value( + [ + firstVideoDevice, + createJSInteropWrapper( + FakeMediaDeviceInfo('2', 'Audio Input 2', MediaDeviceKind.audioInput), + ) + as MediaDeviceInfo, + createJSInteropWrapper( + FakeMediaDeviceInfo('3', 'Audio Output 3', MediaDeviceKind.audioOutput), + ) + as MediaDeviceInfo, + secondVideoDevice, + ].toJS, + ).toJS; + }.toJS; + + // Mock camera service to return the first video stream + // for the first video device. + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: firstVideoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(firstVideoStream)); + + // Mock camera service to return the second video stream + // for the second video device. + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: secondVideoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(secondVideoStream)); + + // Mock camera service to return a user facing mode + // for the first video stream. + when( + cameraService.getFacingModeForVideoTrack(firstVideoStream.getVideoTracks().toDart.first), + ).thenReturn('user'); + + when( + cameraService.mapFacingModeToLensDirection('user'), + ).thenReturn(CameraLensDirection.front); + + // Mock camera service to return an environment facing mode + // for the second video stream. + when( + cameraService.getFacingModeForVideoTrack(secondVideoStream.getVideoTracks().toDart.first), + ).thenReturn('environment'); + + when( + cameraService.mapFacingModeToLensDirection('environment'), + ).thenReturn(CameraLensDirection.back); + + final List cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + name: firstVideoDevice.label, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + name: secondVideoDevice.label, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ]), + ); + }); + + testWidgets('sets camera metadata ' + 'for the camera description', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final videoStream = + createJSInteropWrapper( + FakeMediaStream([ + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ]), + ) + as MediaStream; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when( + cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), + ).thenReturn('left'); + + when( + cameraService.mapFacingModeToLensDirection('left'), + ).thenReturn(CameraLensDirection.external); + + final CameraDescription camera = (await CameraPlatform.instance.availableCameras()).first; + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + camera: CameraMetadata(deviceId: videoDevice.deviceId, facingMode: 'left'), + }), + ); + }); + + testWidgets('releases the video stream ' + 'of a video input device', (WidgetTester tester) async { + final videoDevice = + createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) + as MediaDeviceInfo; + + final tracks = []; + final stops = List.generate(2, (_) => false); + for (var i = 0; i < stops.length; i++) { + final track = MockMediaStreamTrack(); + track.stop = () { + stops[i] = true; + }.toJS; + tracks.add(createJSInteropWrapper(track) as MediaStreamTrack); + } + + final videoStream = createJSInteropWrapper(FakeMediaStream(tracks)) as MediaStream; + + mockMediaDevices.enumerateDevices = () { + return Future>.value([videoDevice].toJS).toJS; + }.toJS; + + when( + cameraService.getMediaStreamForOptions( + CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + final List _ = await CameraPlatform.instance.availableCameras(); + + expect(stops.every((bool e) => e), isTrue); + }); + + group('throws CameraException', () { + testWidgets('when MediaDevices.enumerateDevices throws DomException', ( + WidgetTester tester, + ) async { + final exception = DOMException('UnknownError'); + + mockMediaDevices.enumerateDevices = () { + throw exception; + // ignore: dead_code + return Future>.value([].toJS).toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.name), + ), + ); + }); + + testWidgets('when CameraService.getMediaStreamForOptions ' + 'throws CameraWebException', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.security, 'description'); + + when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); + + expect( + CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when CameraService.getMediaStreamForOptions ' + 'throws PlatformException', (WidgetTester tester) async { + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having((CameraException e) => e.code, 'code', exception.code), + ), + ); + }); + }); + }); + + group('createCamera', () { + group('creates a camera', () { + const ultraHighResolutionSize = Size(3840, 2160); + const maxResolutionSize = Size(3840, 2160); + + const cameraDescription = CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + + const cameraMetadata = CameraMetadata(deviceId: 'deviceId', facingMode: 'user'); + + setUp(() { + // Add metadata for the camera description. + (CameraPlatform.instance as CameraPlugin).camerasMetadata[cameraDescription] = + cameraMetadata; + + when(cameraService.mapFacingModeToCameraType('user')).thenReturn(CameraType.user); + }); + + testWidgets('with appropriate options', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + ResolutionPreset.ultraHigh, + enableAudio: true, + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isTrue); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with appropriate createCameraWithSettings options', ( + WidgetTester tester, + ) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCameraWithSettings( + cameraDescription, + const MediaSettings( + resolutionPreset: ResolutionPreset.ultraHigh, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: true, + ), + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isTrue); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCamera(cameraDescription, null); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.textureId, cameraId); + expect(camera.options.audio.enabled, isFalse); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + + testWidgets('with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified ' + 'using createCameraWithSettings', (WidgetTester tester) async { + when( + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final int cameraId = await CameraPlatform.instance.createCameraWithSettings( + cameraDescription, + const MediaSettings(resolutionPreset: ResolutionPreset.max), + ); + + final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; + + expect(camera, isA()); + expect(camera!.options.audio.enabled, isFalse); + expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); + expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); + expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); + expect(camera.options.video.deviceId, cameraMetadata.deviceId); + }); + }); + + testWidgets('throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.createCamera( + const CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.ultraHigh, + ), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.missingMetadata.toString(), + ), + ), + ); + }); + + testWidgets('throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description ' + 'using createCameraWithSettings', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.createCameraWithSettings( + const CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + const MediaSettings( + resolutionPreset: ResolutionPreset.low, + fps: 15, + videoBitrate: 200000, + audioBitrate: 32000, + enableAudio: true, + ), + ), + throwsA( + isA().having( + (CameraException e) => e.code, + 'code', + CameraErrorCode.missingMetadata.toString(), + ), + ), + ); + }); + }); + + group('initializeCamera', () { + late Camera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + }); + + testWidgets('initializes and plays the camera', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + verify(camera.initialize()).called(1); + verify(camera.play()).called(1); + }); + + testWidgets('starts listening to the camera video error and abort events', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + }); + + testWidgets('starts listening to the camera ended events', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(endedStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(endedStreamController.hasListener, isTrue); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws CameraWebException', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'description', + ); + + when(camera.initialize()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart new file mode 100644 index 000000000000..0b05a7ffdfb9 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_lifecycle_and_events_test.dart @@ -0,0 +1,848 @@ +// 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + testWidgets('CameraPlugin is the live instance', (WidgetTester tester) async { + expect(CameraPlatform.instance, isA()); + }); + group('dispose', () { + late Camera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + when(camera.dispose()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(videoElement), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + }); + + testWidgets('disposes the correct camera', (WidgetTester tester) async { + const firstCameraId = 0; + const secondCameraId = 1; + + final firstCamera = MockCamera(); + final secondCamera = MockCamera(); + + when(firstCamera.dispose()).thenAnswer((_) => Future.value()); + when(secondCamera.dispose()).thenAnswer((_) => Future.value()); + + // Save cameras in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras.addAll({ + firstCameraId: firstCamera, + secondCameraId: secondCamera, + }); + + // Dispose the first camera. + await CameraPlatform.instance.dispose(firstCameraId); + + // The first camera should be disposed. + verify(firstCamera.dispose()).called(1); + verifyNever(secondCamera.dispose()); + + // The first camera should be removed from the camera plugin. + expect( + (CameraPlatform.instance as CameraPlugin).cameras, + equals({secondCameraId: secondCamera}), + ); + }); + + testWidgets('cancels the camera video error and abort subscriptions', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera ended subscriptions', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(endedStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera video recording error subscriptions', ( + WidgetTester tester, + ) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets('with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when dispose throws DomException', (WidgetTester tester) async { + final camera = MockCamera(); + final exception = DOMException('InvalidAccessError'); + + when(camera.dispose()).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('getCamera', () { + testWidgets('returns the correct camera', (WidgetTester tester) async { + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect((CameraPlatform.instance as CameraPlugin).getCamera(cameraId), equals(camera)); + }); + + testWidgets('throws PlatformException ' + 'with notFound error ' + 'if the camera does not exist', (WidgetTester tester) async { + expect( + () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + }); + + group('events', () { + late MockCamera camera; + late MockVideoElement mockVideoElement; + late HTMLVideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + mockVideoElement = MockVideoElement(); + videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize()).thenReturn(const Size(10, 10)); + when(camera.initialize()).thenAnswer((_) => Future.value()); + when(camera.play()).thenAnswer((_) => Future.value()); + + when(camera.videoElement).thenReturn(videoElement); + + final errorProvider = MockEventStreamProvider(); + final abortProvider = MockEventStreamProvider(); + + (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; + (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; + + when( + errorProvider.forElement(any), + ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when( + abortProvider.forElement(any), + ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); + + when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording()).thenAnswer((_) async {}); + }); + + testWidgets('onCameraInitialized emits a CameraInitializedEvent ' + 'on initializeCamera', (WidgetTester tester) async { + // Mock the camera to use a blank video stream of size 1280x720. + const videoSize = Size(1280, 720); + + videoElement = getVideoElementWithBlankStream(videoSize); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: cameraId), + ).thenAnswer((_) async => videoElement.captureStream()); + + final camera = Camera(textureId: cameraId, cameraService: cameraService); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = CameraPlatform.instance + .onCameraInitialized(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect( + await streamQueue.next, + equals( + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('onCameraResolutionChanged emits an empty stream', (WidgetTester tester) async { + final Stream stream = CameraPlatform.instance + .onCameraResolutionChanged(cameraId); + expect(await stream.isEmpty, isTrue); + }); + + testWidgets('onCameraClosing emits a CameraClosingEvent ' + 'on the camera ended event', (WidgetTester tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = CameraPlatform.instance.onCameraClosing( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + endedStreamController.add( + createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, + ); + + expect(await streamQueue.next, equals(const CameraClosingEvent(cameraId))); + + await streamQueue.cancel(); + }); + + group('onCameraError', () { + setUp(() { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with a message', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = + createJSInteropWrapper( + FakeMediaError(MediaError.MEDIA_ERR_NETWORK, 'A network error occurred.'), + ) + as MediaError; + + final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); + + mockVideoElement.error = error; + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent(cameraId, 'Error code: $errorCode, error message: ${error.message}'), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with no message', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = + createJSInteropWrapper(FakeMediaError(MediaError.MEDIA_ERR_NETWORK)) as MediaError; + final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); + + mockVideoElement.error = error; + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: $errorCode, error message: No further diagnostic information can be determined or provided.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video abort event', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + abortStreamController.add(Event('abort')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on takePicture error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.takePicture()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.takePicture(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on setFlashMode error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.setFlashMode(any)).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on getMaxZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMaxZoomLevel()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on getMinZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMinZoomLevel()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.getMinZoomLevel(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on setZoomLevel error', (WidgetTester tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.setZoomLevel(any)).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on resumePreview error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); + + when(camera.play()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.resumePreview(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on startVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); + + when(camera.startVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on the camera video recording error event', (WidgetTester tester) async { + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + + final errorEvent = + createJSInteropWrapper(FakeErrorEvent('type', 'message')) as ErrorEvent; + + videoRecordingErrorController.add(errorEvent); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on stopVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.stopVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on pauseVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.pauseVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a CameraErrorEvent ' + 'on resumeVideoRecording error', (WidgetTester tester) async { + final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); + + when(camera.resumeVideoRecording()).thenThrow(exception); + + final Stream eventStream = CameraPlatform.instance.onCameraError( + cameraId, + ); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA(isA()), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + + testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', (WidgetTester tester) async { + final camera = MockCamera(); + final capturedVideo = XFile('/bogus/test'); + final stream = Stream.value( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), + ); + when(camera.onVideoRecordedEvent).thenAnswer((_) => stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final streamQueue = StreamQueue( + CameraPlatform.instance.onVideoRecordedEvent(cameraId), + ); + + expect( + await streamQueue.next, + equals(VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)), + ); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart new file mode 100644 index 000000000000..1272d691df80 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_orientation_test.dart @@ -0,0 +1,321 @@ +// 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +// ignore_for_file: implementation_imports +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 1; + + late MockWindow mockWindow; + late MockNavigator mockNavigator; + late MockMediaDevices mockMediaDevices; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late HTMLVideoElement videoElement; + + late MockScreen mockScreen; + late MockScreenOrientation mockScreenOrientation; + + late Screen screen; + late ScreenOrientation screenOrientation; + + late MockDocument mockDocument; + late MockElement mockDocumentElement; + + late Document document; + late Element documentElement; + + late MockCameraService cameraService; + + setUp(() async { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + mockMediaDevices = MockMediaDevices(); + + window = createJSInteropWrapper(mockWindow) as Window; + navigator = createJSInteropWrapper(mockNavigator) as Navigator; + mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; + + mockWindow.navigator = navigator; + mockNavigator.mediaDevices = mediaDevices; + + videoElement = getVideoElementWithBlankStream(const Size(10, 10)); + + mockScreen = MockScreen(); + mockScreenOrientation = MockScreenOrientation(); + + screen = createJSInteropWrapper(mockScreen) as Screen; + screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; + + mockScreen.orientation = screenOrientation; + mockWindow.screen = screen; + + mockDocument = MockDocument(); + mockDocumentElement = MockElement(); + + document = createJSInteropWrapper(mockDocument) as Document; + documentElement = createJSInteropWrapper(mockDocumentElement) as Element; + + mockDocument.documentElement = documentElement; + mockWindow.document = document; + + cameraService = MockCameraService(); + + when( + cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), + ).thenAnswer((_) async => videoElement.captureStream()); + + CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; + }); + + group('lockCaptureOrientation', () { + setUp(() { + when( + cameraService.mapDeviceOrientationToOrientationType(any), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets('requests full-screen mode ' + 'on documentElement', (WidgetTester tester) async { + var fullscreenCalls = 0; + mockDocumentElement.requestFullscreen = ([FullscreenOptions? options]) { + fullscreenCalls++; + return Future.value().toJS; + }.toJS; + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ); + + expect(fullscreenCalls, 1); + }); + + testWidgets('locks the capture orientation ' + 'based on the given device orientation', (WidgetTester tester) async { + when( + cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), + ).thenReturn(OrientationType.landscapeSecondary); + + final capturedTypes = []; + mockScreenOrientation.lock = (OrientationLockType orientation) { + capturedTypes.add(orientation); + return Future.value().toJS; + }.toJS; + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + verify( + cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), + ).called(1); + + expect(capturedTypes.length, 1); + expect(capturedTypes[0], OrientationType.landscapeSecondary); + }); + + group('throws PlatformException', () { + testWidgets('with orientationNotSupported error ' + 'when documentElement is not available', (WidgetTester tester) async { + mockDocument.documentElement = null; + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + + mockDocument.documentElement = documentElement; + }); + + testWidgets('when lock throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + mockScreenOrientation.lock = (OrientationLockType orientation) { + throw exception; + // ignore: dead_code + return Future.value().toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitDown, + ), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('unlockCaptureOrientation', () { + setUp(() { + when( + cameraService.mapDeviceOrientationToOrientationType(any), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets('unlocks the capture orientation', (WidgetTester tester) async { + var unlocks = 0; + mockScreenOrientation.unlock = () { + unlocks++; + }.toJS; + + await CameraPlatform.instance.unlockCaptureOrientation(cameraId); + + expect(unlocks, 1); + }); + + group('throws PlatformException', () { + testWidgets('with orientationNotSupported error ' + 'when documentElement is not available', (WidgetTester tester) async { + mockDocument.documentElement = null; + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + + mockDocument.documentElement = documentElement; + }); + + testWidgets('when unlock throws DomException', (WidgetTester tester) async { + final exception = DOMException('NotAllowedError'); + + mockScreenOrientation.unlock = () { + throw exception; + // ignore: dead_code + return Future.value().toJS; + }.toJS; + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), + throwsA( + isA().having( + (PlatformException e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('onDeviceOrientationChanged', () { + final eventStreamController = StreamController(); + + setUp(() { + final provider = MockEventStreamProvider(); + (CameraPlatform.instance as CameraPlugin).orientationOnChangeProvider = provider; + when(provider.forTarget(any)).thenAnswer((_) => eventStreamController.stream); + }); + + testWidgets('emits the initial DeviceOrientationChangedEvent', (WidgetTester tester) async { + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitPrimary), + ).thenReturn(DeviceOrientation.portraitUp); + + // Set the initial screen orientation to portraitPrimary. + mockScreenOrientation.type = OrientationType.portraitPrimary; + + final Stream eventStream = CameraPlatform.instance + .onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp)), + ); + + await streamQueue.cancel(); + }); + + testWidgets('emits a DeviceOrientationChangedEvent ' + 'when the screen orientation is changed', (WidgetTester tester) async { + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.landscapePrimary), + ).thenReturn(DeviceOrientation.landscapeLeft); + + when( + cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitSecondary), + ).thenReturn(DeviceOrientation.portraitDown); + + final Stream eventStream = CameraPlatform.instance + .onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + // Change the screen orientation to landscapePrimary and + // emit an event on the screenOrientation.onChange stream. + mockScreenOrientation.type = OrientationType.landscapePrimary; + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft)), + ); + + // Change the screen orientation to portraitSecondary and + // emit an event on the screenOrientation.onChange stream. + mockScreenOrientation.type = OrientationType.portraitSecondary; + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitDown)), + ); + + await streamQueue.cancel(); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart deleted file mode 100644 index 089b39264814..000000000000 --- a/packages/camera/camera_web/example/integration_test/camera_web_test.dart +++ /dev/null @@ -1,2677 +0,0 @@ -// 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. - -// ignore_for_file: only_throw_errors - -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:math'; - -import 'package:async/async.dart'; -import 'package:camera_platform_interface/camera_platform_interface.dart'; -import 'package:camera_web/camera_web.dart'; -// ignore_for_file: implementation_imports -import 'package:camera_web/src/camera.dart'; -import 'package:camera_web/src/types/types.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart' as widgets; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:web/web.dart' hide MediaDeviceKind, OrientationType; - -import 'helpers/helpers.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('CameraPlugin', () { - const cameraId = 1; - - late MockWindow mockWindow; - late MockNavigator mockNavigator; - late MockMediaDevices mockMediaDevices; - - late Window window; - late Navigator navigator; - late MediaDevices mediaDevices; - - late HTMLVideoElement videoElement; - - late MockScreen mockScreen; - late MockScreenOrientation mockScreenOrientation; - - late Screen screen; - late ScreenOrientation screenOrientation; - - late MockDocument mockDocument; - late MockElement mockDocumentElement; - - late Document document; - late Element documentElement; - - late MockCameraService cameraService; - - setUp(() async { - mockWindow = MockWindow(); - mockNavigator = MockNavigator(); - mockMediaDevices = MockMediaDevices(); - - window = createJSInteropWrapper(mockWindow) as Window; - navigator = createJSInteropWrapper(mockNavigator) as Navigator; - mediaDevices = createJSInteropWrapper(mockMediaDevices) as MediaDevices; - - mockWindow.navigator = navigator; - mockNavigator.mediaDevices = mediaDevices; - - videoElement = getVideoElementWithBlankStream(const Size(10, 10)); - - mockScreen = MockScreen(); - mockScreenOrientation = MockScreenOrientation(); - - screen = createJSInteropWrapper(mockScreen) as Screen; - screenOrientation = createJSInteropWrapper(mockScreenOrientation) as ScreenOrientation; - - mockScreen.orientation = screenOrientation; - mockWindow.screen = screen; - - mockDocument = MockDocument(); - mockDocumentElement = MockElement(); - - document = createJSInteropWrapper(mockDocument) as Document; - documentElement = createJSInteropWrapper(mockDocumentElement) as Element; - - mockDocument.documentElement = documentElement; - mockWindow.document = document; - - cameraService = MockCameraService(); - - when( - cameraService.getMediaStreamForOptions(any, cameraId: anyNamed('cameraId')), - ).thenAnswer((_) async => videoElement.captureStream()); - - CameraPlatform.instance = CameraPlugin(cameraService: cameraService)..window = window; - }); - - testWidgets('CameraPlugin is the live instance', (WidgetTester tester) async { - expect(CameraPlatform.instance, isA()); - }); - - group('availableCameras', () { - setUp(() { - when(cameraService.getFacingModeForVideoTrack(any)).thenReturn(null); - - mockMediaDevices.enumerateDevices = () { - return Future>.value([].toJS).toJS; - }.toJS; - }); - - testWidgets('requests video permissions', (WidgetTester tester) async { - final List _ = await CameraPlatform.instance.availableCameras(); - - verify(cameraService.getMediaStreamForOptions(const CameraOptions())).called(1); - }); - - testWidgets('releases the camera stream ' - 'used to request video permissions', (WidgetTester tester) async { - final mockVideoTrack = MockMediaStreamTrack(); - final videoTrack = createJSInteropWrapper(mockVideoTrack) as MediaStreamTrack; - - var videoTrackStopped = false; - mockVideoTrack.stop = () { - videoTrackStopped = true; - }.toJS; - - when(cameraService.getMediaStreamForOptions(const CameraOptions())).thenAnswer( - (_) => Future.value( - createJSInteropWrapper(FakeMediaStream([videoTrack])) as MediaStream, - ), - ); - - final List _ = await CameraPlatform.instance.availableCameras(); - - expect(videoTrackStopped, isTrue); - }); - - testWidgets('gets a video stream ' - 'for a video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verify( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).called(1); - }); - - testWidgets('does not get a video stream ' - 'for the video input device ' - 'with an empty device id', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verifyNever( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ); - }); - - testWidgets('gets the facing mode ' - 'from the first available video track ' - 'of the video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final videoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - final List _ = await CameraPlatform.instance.availableCameras(); - - verify( - cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), - ).called(1); - }); - - testWidgets('returns appropriate camera descriptions ' - 'for multiple video devices ' - 'based on video streams', (WidgetTester tester) async { - final firstVideoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final secondVideoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('4', 'Camera 4', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - // Create a video stream for the first video device. - final firstVideoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - // Create a video stream for the second video device. - final secondVideoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - // Mock media devices to return two video input devices - // and two audio devices. - mockMediaDevices.enumerateDevices = () { - return Future>.value( - [ - firstVideoDevice, - createJSInteropWrapper( - FakeMediaDeviceInfo('2', 'Audio Input 2', MediaDeviceKind.audioInput), - ) - as MediaDeviceInfo, - createJSInteropWrapper( - FakeMediaDeviceInfo('3', 'Audio Output 3', MediaDeviceKind.audioOutput), - ) - as MediaDeviceInfo, - secondVideoDevice, - ].toJS, - ).toJS; - }.toJS; - - // Mock camera service to return the first video stream - // for the first video device. - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: firstVideoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(firstVideoStream)); - - // Mock camera service to return the second video stream - // for the second video device. - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: secondVideoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(secondVideoStream)); - - // Mock camera service to return a user facing mode - // for the first video stream. - when( - cameraService.getFacingModeForVideoTrack(firstVideoStream.getVideoTracks().toDart.first), - ).thenReturn('user'); - - when( - cameraService.mapFacingModeToLensDirection('user'), - ).thenReturn(CameraLensDirection.front); - - // Mock camera service to return an environment facing mode - // for the second video stream. - when( - cameraService.getFacingModeForVideoTrack(secondVideoStream.getVideoTracks().toDart.first), - ).thenReturn('environment'); - - when( - cameraService.mapFacingModeToLensDirection('environment'), - ).thenReturn(CameraLensDirection.back); - - final List cameras = await CameraPlatform.instance.availableCameras(); - - // Expect two cameras and ignore two audio devices. - expect( - cameras, - equals([ - CameraDescription( - name: firstVideoDevice.label, - lensDirection: CameraLensDirection.front, - sensorOrientation: 0, - ), - CameraDescription( - name: secondVideoDevice.label, - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - ]), - ); - }); - - testWidgets('sets camera metadata ' - 'for the camera description', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final videoStream = - createJSInteropWrapper( - FakeMediaStream([ - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ]), - ) - as MediaStream; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - when( - cameraService.getFacingModeForVideoTrack(videoStream.getVideoTracks().toDart.first), - ).thenReturn('left'); - - when( - cameraService.mapFacingModeToLensDirection('left'), - ).thenReturn(CameraLensDirection.external); - - final CameraDescription camera = (await CameraPlatform.instance.availableCameras()).first; - - expect( - (CameraPlatform.instance as CameraPlugin).camerasMetadata, - equals({ - camera: CameraMetadata(deviceId: videoDevice.deviceId, facingMode: 'left'), - }), - ); - }); - - testWidgets('releases the video stream ' - 'of a video input device', (WidgetTester tester) async { - final videoDevice = - createJSInteropWrapper(FakeMediaDeviceInfo('1', 'Camera 1', MediaDeviceKind.videoInput)) - as MediaDeviceInfo; - - final tracks = []; - final stops = List.generate(2, (_) => false); - for (var i = 0; i < stops.length; i++) { - final track = MockMediaStreamTrack(); - track.stop = () { - stops[i] = true; - }.toJS; - tracks.add(createJSInteropWrapper(track) as MediaStreamTrack); - } - - final videoStream = createJSInteropWrapper(FakeMediaStream(tracks)) as MediaStream; - - mockMediaDevices.enumerateDevices = () { - return Future>.value([videoDevice].toJS).toJS; - }.toJS; - - when( - cameraService.getMediaStreamForOptions( - CameraOptions(video: VideoConstraints(deviceId: videoDevice.deviceId)), - ), - ).thenAnswer((_) => Future.value(videoStream)); - - final List _ = await CameraPlatform.instance.availableCameras(); - - expect(stops.every((bool e) => e), isTrue); - }); - - group('throws CameraException', () { - testWidgets('when MediaDevices.enumerateDevices throws DomException', ( - WidgetTester tester, - ) async { - final exception = DOMException('UnknownError'); - - mockMediaDevices.enumerateDevices = () { - throw exception; - // ignore: dead_code - return Future>.value([].toJS).toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.availableCameras(), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.name), - ), - ); - }); - - testWidgets('when CameraService.getMediaStreamForOptions ' - 'throws CameraWebException', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.security, 'description'); - - when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); - - expect( - CameraPlatform.instance.availableCameras(), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - - testWidgets('when CameraService.getMediaStreamForOptions ' - 'throws PlatformException', (WidgetTester tester) async { - final exception = PlatformException( - code: CameraErrorCode.notSupported.toString(), - message: 'message', - ); - - when(cameraService.getMediaStreamForOptions(any)).thenThrow(exception); - - expect( - () => CameraPlatform.instance.availableCameras(), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.code), - ), - ); - }); - }); - }); - - group('createCamera', () { - group('creates a camera', () { - const ultraHighResolutionSize = Size(3840, 2160); - const maxResolutionSize = Size(3840, 2160); - - const cameraDescription = CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.front, - sensorOrientation: 0, - ); - - const cameraMetadata = CameraMetadata(deviceId: 'deviceId', facingMode: 'user'); - - setUp(() { - // Add metadata for the camera description. - (CameraPlatform.instance as CameraPlugin).camerasMetadata[cameraDescription] = - cameraMetadata; - - when(cameraService.mapFacingModeToCameraType('user')).thenReturn(CameraType.user); - }); - - testWidgets('with appropriate options', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - ).thenReturn(ultraHighResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCamera( - cameraDescription, - ResolutionPreset.ultraHigh, - enableAudio: true, - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isTrue); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with appropriate createCameraWithSettings options', ( - WidgetTester tester, - ) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), - ).thenReturn(ultraHighResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCameraWithSettings( - cameraDescription, - const MediaSettings( - resolutionPreset: ResolutionPreset.ultraHigh, - videoBitrate: 200000, - audioBitrate: 32000, - enableAudio: true, - ), - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isTrue); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, ultraHighResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, ultraHighResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with a max resolution preset ' - 'and enabled audio set to false ' - 'when no options are specified', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - ).thenReturn(maxResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCamera(cameraDescription, null); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.textureId, cameraId); - expect(camera.options.audio.enabled, isFalse); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - - testWidgets('with a max resolution preset ' - 'and enabled audio set to false ' - 'when no options are specified ' - 'using createCameraWithSettings', (WidgetTester tester) async { - when( - cameraService.mapResolutionPresetToSize(ResolutionPreset.max), - ).thenReturn(maxResolutionSize); - - final int cameraId = await CameraPlatform.instance.createCameraWithSettings( - cameraDescription, - const MediaSettings(resolutionPreset: ResolutionPreset.max), - ); - - final Camera? camera = (CameraPlatform.instance as CameraPlugin).cameras[cameraId]; - - expect(camera, isA()); - expect(camera!.options.audio.enabled, isFalse); - expect(camera.options.video.facingMode, equals(FacingModeConstraint(CameraType.user))); - expect(camera.options.video.width!.ideal, maxResolutionSize.width.toInt()); - expect(camera.options.video.height!.ideal, maxResolutionSize.height.toInt()); - expect(camera.options.video.deviceId, cameraMetadata.deviceId); - }); - }); - - testWidgets('throws CameraException ' - 'with missingMetadata error ' - 'if there is no metadata ' - 'for the given camera description', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.createCamera( - const CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - ResolutionPreset.ultraHigh, - ), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.missingMetadata.toString(), - ), - ), - ); - }); - - testWidgets('throws CameraException ' - 'with missingMetadata error ' - 'if there is no metadata ' - 'for the given camera description ' - 'using createCameraWithSettings', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.createCameraWithSettings( - const CameraDescription( - name: 'name', - lensDirection: CameraLensDirection.back, - sensorOrientation: 0, - ), - const MediaSettings( - resolutionPreset: ResolutionPreset.low, - fps: 15, - videoBitrate: 200000, - audioBitrate: 32000, - enableAudio: true, - ), - ), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.missingMetadata.toString(), - ), - ), - ); - }); - }); - - group('initializeCamera', () { - late Camera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - }); - - testWidgets('initializes and plays the camera', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - - verify(camera.initialize()).called(1); - verify(camera.play()).called(1); - }); - - testWidgets('starts listening to the camera video error and abort events', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect(errorStreamController.hasListener, isTrue); - expect(abortStreamController.hasListener, isTrue); - }); - - testWidgets('starts listening to the camera ended events', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(endedStreamController.hasListener, isFalse); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect(endedStreamController.hasListener, isTrue); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when camera throws CameraWebException', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.permissionDenied, - 'description', - ); - - when(camera.initialize()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - - testWidgets('when camera throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.initializeCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('lockCaptureOrientation', () { - setUp(() { - when( - cameraService.mapDeviceOrientationToOrientationType(any), - ).thenReturn(OrientationType.portraitPrimary); - }); - - testWidgets('requests full-screen mode ' - 'on documentElement', (WidgetTester tester) async { - var fullscreenCalls = 0; - mockDocumentElement.requestFullscreen = ([FullscreenOptions? options]) { - fullscreenCalls++; - return Future.value().toJS; - }.toJS; - - await CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitUp, - ); - - expect(fullscreenCalls, 1); - }); - - testWidgets('locks the capture orientation ' - 'based on the given device orientation', (WidgetTester tester) async { - when( - cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), - ).thenReturn(OrientationType.landscapeSecondary); - - final capturedTypes = []; - mockScreenOrientation.lock = (OrientationLockType orientation) { - capturedTypes.add(orientation); - return Future.value().toJS; - }.toJS; - - await CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.landscapeRight, - ); - - verify( - cameraService.mapDeviceOrientationToOrientationType(DeviceOrientation.landscapeRight), - ).called(1); - - expect(capturedTypes.length, 1); - expect(capturedTypes[0], OrientationType.landscapeSecondary); - }); - - group('throws PlatformException', () { - testWidgets('with orientationNotSupported error ' - 'when documentElement is not available', (WidgetTester tester) async { - mockDocument.documentElement = null; - - expect( - () => CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitUp, - ), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.orientationNotSupported.toString(), - ), - ), - ); - - mockDocument.documentElement = documentElement; - }); - - testWidgets('when lock throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - mockScreenOrientation.lock = (OrientationLockType orientation) { - throw exception; - // ignore: dead_code - return Future.value().toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.lockCaptureOrientation( - cameraId, - DeviceOrientation.portraitDown, - ), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('unlockCaptureOrientation', () { - setUp(() { - when( - cameraService.mapDeviceOrientationToOrientationType(any), - ).thenReturn(OrientationType.portraitPrimary); - }); - - testWidgets('unlocks the capture orientation', (WidgetTester tester) async { - var unlocks = 0; - mockScreenOrientation.unlock = () { - unlocks++; - }.toJS; - - await CameraPlatform.instance.unlockCaptureOrientation(cameraId); - - expect(unlocks, 1); - }); - - group('throws PlatformException', () { - testWidgets('with orientationNotSupported error ' - 'when documentElement is not available', (WidgetTester tester) async { - mockDocument.documentElement = null; - - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.orientationNotSupported.toString(), - ), - ), - ); - - mockDocument.documentElement = documentElement; - }); - - testWidgets('when unlock throws DomException', (WidgetTester tester) async { - final exception = DOMException('NotAllowedError'); - - mockScreenOrientation.unlock = () { - throw exception; - // ignore: dead_code - return Future.value().toJS; - }.toJS; - - expect( - () => CameraPlatform.instance.unlockCaptureOrientation(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('takePicture', () { - testWidgets('captures a picture', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedPicture = XFile('/bogus/test'); - - when(camera.takePicture()).thenAnswer((_) async => capturedPicture); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final XFile picture = await CameraPlatform.instance.takePicture(cameraId); - - verify(camera.takePicture()).called(1); - - expect(picture, equals(capturedPicture)); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when takePicture throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.takePicture()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when takePicture throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.takePicture()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.takePicture(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('startVideoRecording', () { - late Camera camera; - - setUp(() { - camera = MockCamera(); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - }); - - testWidgets('starts a video recording', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - - verify(camera.startVideoRecording()).called(1); - }); - - testWidgets('listens to the onVideoRecordingError stream', (WidgetTester tester) async { - final videoRecordingErrorController = StreamController(); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - - expect(videoRecordingErrorController.hasListener, isTrue); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when startVideoRecording throws DomException', (WidgetTester tester) async { - final exception = DOMException('InvalidStateError'); - - when(camera.startVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when startVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.startVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('startVideoCapturing', () { - late Camera camera; - - setUp(() { - camera = MockCamera(); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - }); - - testWidgets('fails if trying to stream', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.startVideoCapturing( - VideoCaptureOptions(cameraId, streamCallback: (CameraImageData imageData) {}), - ), - throwsA(isA()), - ); - }); - }); - - group('stopVideoRecording', () { - testWidgets('stops a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedVideo = XFile('/bogus/test'); - - when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final XFile video = await CameraPlatform.instance.stopVideoRecording(cameraId); - - verify(camera.stopVideoRecording()).called(1); - - expect(video, capturedVideo); - }); - - testWidgets('stops listening to the onVideoRecordingError stream', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final videoRecordingErrorController = StreamController(); - final capturedVideo = XFile('/bogus/test'); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - - when(camera.stopVideoRecording()).thenAnswer((_) async => capturedVideo); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.startVideoRecording(cameraId); - final XFile _ = await CameraPlatform.instance.stopVideoRecording(cameraId); - - expect(videoRecordingErrorController.hasListener, isFalse); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when stopVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when stopVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('pauseVideoRecording', () { - testWidgets('pauses a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.pauseVideoRecording()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.pauseVideoRecording(cameraId); - - verify(camera.pauseVideoRecording()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when pauseVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when pauseVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('resumeVideoRecording', () { - testWidgets('resumes a video recording', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.resumeVideoRecording()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.resumeVideoRecording(cameraId); - - verify(camera.resumeVideoRecording()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when resumeVideoRecording throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidStateError'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when resumeVideoRecording throws CameraWebException', ( - WidgetTester tester, - ) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('setFlashMode', () { - testWidgets('calls setFlashMode on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const FlashMode flashMode = FlashMode.always; - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.setFlashMode(cameraId, flashMode); - - verify(camera.setFlashMode(flashMode)).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when setFlashMode throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when setFlashMode throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.torch), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - testWidgets('setExposureMode throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposureMode(cameraId, ExposureMode.auto), - throwsUnimplementedError, - ); - }); - - testWidgets('setExposurePoint throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposurePoint(cameraId, const Point(0, 0)), - throwsUnimplementedError, - ); - }); - - testWidgets('getMinExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getMinExposureOffset(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('getMaxExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getMaxExposureOffset(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('getExposureOffsetStepSize throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), - throwsUnimplementedError, - ); - }); - - testWidgets('setExposureOffset throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setExposureOffset(cameraId, 0), - throwsUnimplementedError, - ); - }); - - testWidgets('setFocusMode throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFocusMode(cameraId, FocusMode.auto), - throwsUnimplementedError, - ); - }); - - testWidgets('setFocusPoint throws UnimplementedError', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.setFocusPoint(cameraId, const Point(0, 0)), - throwsUnimplementedError, - ); - }); - - group('getMaxZoomLevel', () { - testWidgets('calls getMaxZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const maximumZoomLevel = 100.0; - - when(camera.getMaxZoomLevel()).thenReturn(maximumZoomLevel); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(await CameraPlatform.instance.getMaxZoomLevel(cameraId), equals(maximumZoomLevel)); - - verify(camera.getMaxZoomLevel()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when getMaxZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when getMaxZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('getMinZoomLevel', () { - testWidgets('calls getMinZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - const minimumZoomLevel = 100.0; - - when(camera.getMinZoomLevel()).thenReturn(minimumZoomLevel); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect(await CameraPlatform.instance.getMinZoomLevel(cameraId), equals(minimumZoomLevel)); - - verify(camera.getMinZoomLevel()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when getMinZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when getMinZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('setZoomLevel', () { - testWidgets('calls setZoomLevel on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - const zoom = 100.0; - - await CameraPlatform.instance.setZoomLevel(cameraId, zoom); - - verify(camera.setZoomLevel(zoom)).called(1); - }); - - group('throws CameraException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when setZoomLevel throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.name), - ), - ); - }); - - testWidgets('when setZoomLevel throws PlatformException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = PlatformException( - code: CameraErrorCode.notSupported.toString(), - message: 'message', - ); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having((CameraException e) => e.code, 'code', exception.code), - ), - ); - }); - - testWidgets('when setZoomLevel throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA( - isA().having( - (CameraException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - group('pausePreview', () { - testWidgets('calls pause on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.pausePreview(cameraId); - - verify(camera.pause()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.pausePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when pause throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.pause()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.pausePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('resumePreview', () { - testWidgets('calls play on the camera', (WidgetTester tester) async { - final camera = MockCamera(); - - when(camera.play()).thenAnswer((_) async {}); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.resumePreview(cameraId); - - verify(camera.play()).called(1); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when play throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('NotSupportedError'); - - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - - testWidgets('when play throws CameraWebException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); - - when(camera.play()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.code.toString(), - ), - ), - ); - }); - }); - }); - - testWidgets('buildPreview returns an HtmlElementView ' - 'with an appropriate view type', (WidgetTester tester) async { - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - CameraPlatform.instance.buildPreview(cameraId), - isA().having( - (widgets.HtmlElementView view) => view.viewType, - 'viewType', - camera.getViewType(), - ), - ); - }); - - group('dispose', () { - late Camera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - late StreamController videoRecordingErrorController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - videoRecordingErrorController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - when(camera.dispose()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(videoElement), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - }); - - testWidgets('disposes the correct camera', (WidgetTester tester) async { - const firstCameraId = 0; - const secondCameraId = 1; - - final firstCamera = MockCamera(); - final secondCamera = MockCamera(); - - when(firstCamera.dispose()).thenAnswer((_) => Future.value()); - when(secondCamera.dispose()).thenAnswer((_) => Future.value()); - - // Save cameras in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras.addAll({ - firstCameraId: firstCamera, - secondCameraId: secondCamera, - }); - - // Dispose the first camera. - await CameraPlatform.instance.dispose(firstCameraId); - - // The first camera should be disposed. - verify(firstCamera.dispose()).called(1); - verifyNever(secondCamera.dispose()); - - // The first camera should be removed from the camera plugin. - expect( - (CameraPlatform.instance as CameraPlugin).cameras, - equals({secondCameraId: secondCamera}), - ); - }); - - testWidgets('cancels the camera video error and abort subscriptions', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(errorStreamController.hasListener, isFalse); - expect(abortStreamController.hasListener, isFalse); - }); - - testWidgets('cancels the camera ended subscriptions', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(endedStreamController.hasListener, isFalse); - }); - - testWidgets('cancels the camera video recording error subscriptions', ( - WidgetTester tester, - ) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.startVideoRecording(cameraId); - await CameraPlatform.instance.dispose(cameraId); - - expect(videoRecordingErrorController.hasListener, isFalse); - }); - - group('throws PlatformException', () { - testWidgets('with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - - testWidgets('when dispose throws DomException', (WidgetTester tester) async { - final camera = MockCamera(); - final exception = DOMException('InvalidAccessError'); - - when(camera.dispose()).thenThrow(exception); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect( - () => CameraPlatform.instance.dispose(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - exception.name, - ), - ), - ); - }); - }); - }); - - group('getCamera', () { - testWidgets('returns the correct camera', (WidgetTester tester) async { - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - expect((CameraPlatform.instance as CameraPlugin).getCamera(cameraId), equals(camera)); - }); - - testWidgets('throws PlatformException ' - 'with notFound error ' - 'if the camera does not exist', (WidgetTester tester) async { - expect( - () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), - throwsA( - isA().having( - (PlatformException e) => e.code, - 'code', - CameraErrorCode.notFound.toString(), - ), - ), - ); - }); - }); - - group('events', () { - late MockCamera camera; - late MockVideoElement mockVideoElement; - late HTMLVideoElement videoElement; - - late StreamController errorStreamController, abortStreamController; - late StreamController endedStreamController; - late StreamController videoRecordingErrorController; - - setUp(() { - camera = MockCamera(); - mockVideoElement = MockVideoElement(); - videoElement = createJSInteropWrapper(mockVideoElement) as HTMLVideoElement; - - errorStreamController = StreamController(); - abortStreamController = StreamController(); - endedStreamController = StreamController(); - videoRecordingErrorController = StreamController(); - - when(camera.getVideoSize()).thenReturn(const Size(10, 10)); - when(camera.initialize()).thenAnswer((_) => Future.value()); - when(camera.play()).thenAnswer((_) => Future.value()); - - when(camera.videoElement).thenReturn(videoElement); - - final errorProvider = MockEventStreamProvider(); - final abortProvider = MockEventStreamProvider(); - - (CameraPlatform.instance as CameraPlugin).videoElementOnErrorProvider = errorProvider; - (CameraPlatform.instance as CameraPlugin).videoElementOnAbortProvider = abortProvider; - - when( - errorProvider.forElement(any), - ).thenAnswer((_) => FakeElementStream(errorStreamController.stream)); - when( - abortProvider.forElement(any), - ).thenAnswer((_) => FakeElementStream(abortStreamController.stream)); - - when(camera.onEnded).thenAnswer((_) => endedStreamController.stream); - - when(camera.onVideoRecordingError).thenAnswer((_) => videoRecordingErrorController.stream); - - when(camera.startVideoRecording()).thenAnswer((_) async {}); - }); - - testWidgets('onCameraInitialized emits a CameraInitializedEvent ' - 'on initializeCamera', (WidgetTester tester) async { - // Mock the camera to use a blank video stream of size 1280x720. - const videoSize = Size(1280, 720); - - videoElement = getVideoElementWithBlankStream(videoSize); - - when( - cameraService.getMediaStreamForOptions(any, cameraId: cameraId), - ).thenAnswer((_) async => videoElement.captureStream()); - - final camera = Camera(textureId: cameraId, cameraService: cameraService); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final Stream eventStream = CameraPlatform.instance - .onCameraInitialized(cameraId); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - expect( - await streamQueue.next, - equals( - CameraInitializedEvent( - cameraId, - videoSize.width, - videoSize.height, - ExposureMode.auto, - false, - FocusMode.auto, - false, - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('onCameraResolutionChanged emits an empty stream', (WidgetTester tester) async { - final Stream stream = CameraPlatform.instance - .onCameraResolutionChanged(cameraId); - expect(await stream.isEmpty, isTrue); - }); - - testWidgets('onCameraClosing emits a CameraClosingEvent ' - 'on the camera ended event', (WidgetTester tester) async { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final Stream eventStream = CameraPlatform.instance.onCameraClosing( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - endedStreamController.add( - createJSInteropWrapper(MockMediaStreamTrack()) as MediaStreamTrack, - ); - - expect(await streamQueue.next, equals(const CameraClosingEvent(cameraId))); - - await streamQueue.cancel(); - }); - - group('onCameraError', () { - setUp(() { - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video error event ' - 'with a message', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - final error = - createJSInteropWrapper( - FakeMediaError(MediaError.MEDIA_ERR_NETWORK, 'A network error occurred.'), - ) - as MediaError; - - final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); - - mockVideoElement.error = error; - errorStreamController.add(Event('error')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent(cameraId, 'Error code: $errorCode, error message: ${error.message}'), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video error event ' - 'with no message', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - final error = - createJSInteropWrapper(FakeMediaError(MediaError.MEDIA_ERR_NETWORK)) as MediaError; - final CameraErrorCode errorCode = CameraErrorCode.fromMediaError(error); - - mockVideoElement.error = error; - errorStreamController.add(Event('error')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: $errorCode, error message: No further diagnostic information can be determined or provided.', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video abort event', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - - abortStreamController.add(Event('abort')); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - "Error code: ${CameraErrorCode.abort}, error message: The video element's source has not fully loaded.", - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on takePicture error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.takePicture()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.takePicture(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on setFlashMode error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.setFlashMode(any)).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.setFlashMode(cameraId, FlashMode.always), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on getMaxZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.getMaxZoomLevel()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.getMaxZoomLevel(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on getMinZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.getMinZoomLevel()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.getMinZoomLevel(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on setZoomLevel error', (WidgetTester tester) async { - final exception = CameraWebException( - cameraId, - CameraErrorCode.zoomLevelNotSupported, - 'description', - ); - - when(camera.setZoomLevel(any)).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.setZoomLevel(cameraId, 100.0), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on resumePreview error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.unknown, 'description'); - - when(camera.play()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.resumePreview(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on startVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.onVideoRecordingError).thenAnswer((_) => const Stream.empty()); - - when(camera.startVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.startVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on the camera video recording error event', (WidgetTester tester) async { - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - await CameraPlatform.instance.initializeCamera(cameraId); - await CameraPlatform.instance.startVideoRecording(cameraId); - - final errorEvent = - createJSInteropWrapper(FakeErrorEvent('type', 'message')) as ErrorEvent; - - videoRecordingErrorController.add(errorEvent); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on stopVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.stopVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.stopVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on pauseVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.pauseVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.pauseVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a CameraErrorEvent ' - 'on resumeVideoRecording error', (WidgetTester tester) async { - final exception = CameraWebException(cameraId, CameraErrorCode.notStarted, 'description'); - - when(camera.resumeVideoRecording()).thenThrow(exception); - - final Stream eventStream = CameraPlatform.instance.onCameraError( - cameraId, - ); - - final streamQueue = StreamQueue(eventStream); - - expect( - () async => CameraPlatform.instance.resumeVideoRecording(cameraId), - throwsA(isA()), - ); - - expect( - await streamQueue.next, - equals( - CameraErrorEvent( - cameraId, - 'Error code: ${exception.code}, error message: ${exception.description}', - ), - ), - ); - - await streamQueue.cancel(); - }); - }); - - testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', (WidgetTester tester) async { - final camera = MockCamera(); - final capturedVideo = XFile('/bogus/test'); - final stream = Stream.value( - VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), - ); - when(camera.onVideoRecordedEvent).thenAnswer((_) => stream); - - // Save the camera in the camera plugin. - (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; - - final streamQueue = StreamQueue( - CameraPlatform.instance.onVideoRecordedEvent(cameraId), - ); - - expect( - await streamQueue.next, - equals(VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)), - ); - }); - - group('onDeviceOrientationChanged', () { - final eventStreamController = StreamController(); - - setUp(() { - final provider = MockEventStreamProvider(); - (CameraPlatform.instance as CameraPlugin).orientationOnChangeProvider = provider; - when(provider.forTarget(any)).thenAnswer((_) => eventStreamController.stream); - }); - - testWidgets('emits the initial DeviceOrientationChangedEvent', (WidgetTester tester) async { - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitPrimary), - ).thenReturn(DeviceOrientation.portraitUp); - - // Set the initial screen orientation to portraitPrimary. - mockScreenOrientation.type = OrientationType.portraitPrimary; - - final Stream eventStream = CameraPlatform.instance - .onDeviceOrientationChanged(); - - final streamQueue = StreamQueue(eventStream); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp)), - ); - - await streamQueue.cancel(); - }); - - testWidgets('emits a DeviceOrientationChangedEvent ' - 'when the screen orientation is changed', (WidgetTester tester) async { - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.landscapePrimary), - ).thenReturn(DeviceOrientation.landscapeLeft); - - when( - cameraService.mapOrientationTypeToDeviceOrientation(OrientationType.portraitSecondary), - ).thenReturn(DeviceOrientation.portraitDown); - - final Stream eventStream = CameraPlatform.instance - .onDeviceOrientationChanged(); - - final streamQueue = StreamQueue(eventStream); - - // Change the screen orientation to landscapePrimary and - // emit an event on the screenOrientation.onChange stream. - mockScreenOrientation.type = OrientationType.landscapePrimary; - - eventStreamController.add(Event('change')); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft)), - ); - - // Change the screen orientation to portraitSecondary and - // emit an event on the screenOrientation.onChange stream. - mockScreenOrientation.type = OrientationType.portraitSecondary; - - eventStreamController.add(Event('change')); - - expect( - await streamQueue.next, - equals(const DeviceOrientationChangedEvent(DeviceOrientation.portraitDown)), - ); - - await streamQueue.cancel(); - }); - }); - }); - }); -} diff --git a/packages/cross_file/dart_test.yaml b/packages/cross_file/dart_test.yaml index cdb656dbb689..36d5c9b8bcae 100644 --- a/packages/cross_file/dart_test.yaml +++ b/packages/cross_file/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/cross_file/example/dart_test.yaml b/packages/cross_file/example/dart_test.yaml index cdb656dbb689..36d5c9b8bcae 100644 --- a/packages/cross_file/example/dart_test.yaml +++ b/packages/cross_file/example/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/google_identity_services_web/dart_test.yaml b/packages/google_identity_services_web/dart_test.yaml index cdb656dbb689..36d5c9b8bcae 100644 --- a/packages/google_identity_services_web/dart_test.yaml +++ b/packages/google_identity_services_web/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/plugin_platform_interface/dart_test.yaml b/packages/plugin_platform_interface/dart_test.yaml index cdb656dbb689..36d5c9b8bcae 100644 --- a/packages/plugin_platform_interface/dart_test.yaml +++ b/packages/plugin_platform_interface/dart_test.yaml @@ -3,4 +3,17 @@ override_platforms: chrome: settings: executable: chrome - arguments: --no-sandbox + arguments: >- + --no-sandbox + --disable-background-timer-throttling + --disable-renderer-backgrounding + --disable-background-networking + --disable-search-engine-choice-screen + --disable-extensions + --disable-popup-blocking + --disable-translate + --disable-default-apps + --no-default-browser-check + --no-first-run + --password-store=basic + --bwsi diff --git a/packages/shared_preferences/shared_preferences_tool/dart_test.yaml b/packages/shared_preferences/shared_preferences_tool/dart_test.yaml new file mode 100644 index 000000000000..91ec220b8e22 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_tool/dart_test.yaml @@ -0,0 +1 @@ +test_on: vm diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index b3b99c72728c..ca20b053441e 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -306,7 +306,7 @@ void main() { // Due to the duration calculation accuracy between platforms, // the milliseconds on Web will be a slightly different from natives. // The audio was made with 44100 Hz, 192 Kbps CBR, and 32 bits. - expect(controller.value.duration, const Duration(seconds: 5, milliseconds: kIsWeb ? 42 : 41)); + expect(controller.value.duration.inMilliseconds, closeTo(5000, 50)); }); testWidgets('can be played', (WidgetTester tester) async { diff --git a/packages/web_benchmarks/CHANGELOG.md b/packages/web_benchmarks/CHANGELOG.md index a20c00696c62..1db81531bfdb 100644 --- a/packages/web_benchmarks/CHANGELOG.md +++ b/packages/web_benchmarks/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 4.1.2 +* Updates Chrome launch arguments to unblock Chrome 145+ upgrade. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 4.1.1 diff --git a/packages/web_benchmarks/lib/src/browser.dart b/packages/web_benchmarks/lib/src/browser.dart index d632a68e7625..75e22d4b6e11 100644 --- a/packages/web_benchmarks/lib/src/browser.dart +++ b/packages/web_benchmarks/lib/src/browser.dart @@ -98,10 +98,16 @@ class Chrome { final args = [ if (options.userDataDirectory != null) '--user-data-dir=${options.userDataDirectory}', if (url != null) url, - if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox', - if (options.headless) '--headless', - if (withDebugging) '--remote-debugging-port=${options.debugPort}', - '--window-size=${options.windowWidth},${options.windowHeight}', + '--disable-background-timer-throttling', + '--disable-renderer-backgrounding', + '--disable-background-networking', + '--disable-sync', + '--disable-client-side-phishing-detection', + '--disable-notifications', + '--disable-features=GCM', + '--gcm-checkin-url=http://127.0.0.1', + '--gcm-registration-url=http://127.0.0.1', + '--gcm-mcs-endpoint=127.0.0.1:0', '--disable-extensions', '--disable-popup-blocking', // Indicates that the browser is in "browse without sign-in" (Guest session) mode. @@ -110,6 +116,22 @@ class Chrome { '--no-default-browser-check', '--disable-default-apps', '--disable-translate', + '--password-store=basic', + if (io.Platform.isMacOS) '--use-mock-keychain', + '--disable-search-engine-choice-screen', + if (io.Platform.environment['CHROME_NO_SANDBOX'] == 'true') '--no-sandbox', + if (options.headless) ...[ + '--headless', + '--no-sandbox', + if (io.Platform.isLinux) ...[ + '--use-gl=angle', + '--use-angle=swiftshader', + '--enable-unsafe-swiftshader', + '--disable-gpu-sandbox', + ], + ], + if (withDebugging) '--remote-debugging-port=${options.debugPort}', + '--window-size=${options.windowWidth},${options.windowHeight}', ]; final io.Process chromeProcess = await io.Process.start( _findSystemChromeExecutable(), diff --git a/packages/web_benchmarks/pubspec.yaml b/packages/web_benchmarks/pubspec.yaml index 4a687a973f86..f43d83af9d3e 100644 --- a/packages/web_benchmarks/pubspec.yaml +++ b/packages/web_benchmarks/pubspec.yaml @@ -2,7 +2,7 @@ name: web_benchmarks description: A benchmark harness for performance-testing Flutter apps in Chrome. repository: https://github.com/flutter/packages/tree/main/packages/web_benchmarks issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+web_benchmarks%22 -version: 4.1.1 +version: 4.1.2 environment: sdk: ^3.10.0 diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 2d89df0b270f..defffa4204b1 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -51,6 +51,22 @@ class DriveExamplesCommand extends PackageLoopingCommand { static const String _chromeDriverFlag = 'run-chromedriver'; + static const List _webChromeBrowserFlags = [ + '--web-browser-flag=--no-sandbox', + '--web-browser-flag=--disable-background-timer-throttling', + '--web-browser-flag=--disable-renderer-backgrounding', + '--web-browser-flag=--disable-background-networking', + '--web-browser-flag=--disable-search-engine-choice-screen', + '--web-browser-flag=--disable-extensions', + '--web-browser-flag=--disable-popup-blocking', + '--web-browser-flag=--disable-translate', + '--web-browser-flag=--disable-default-apps', + '--web-browser-flag=--no-default-browser-check', + '--web-browser-flag=--no-first-run', + '--web-browser-flag=--password-store=basic', + '--web-browser-flag=--bwsi', + ]; + @override final String name = 'drive-examples'; @@ -139,6 +155,8 @@ class DriveExamplesCommand extends PackageLoopingCommand { if (useWasm) '--wasm', if (platform.environment.containsKey('CHROME_EXECUTABLE')) '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', + ..._webChromeBrowserFlags, + if (platform.isMacOS) '--web-browser-flag=--use-mock-keychain', ], if (getBoolArg(platformWindows)) platformWindows: ['-d', 'windows'], }; diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 81a0d9f4cbaa..aa357e5fec6a 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -13,7 +13,6 @@ import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; import 'package:test/test.dart'; import 'mocks.dart'; @@ -21,10 +20,25 @@ import 'util.dart'; const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; const String _fakeAndroidDevice = 'emulator-1234'; +const List _defaultWebBrowserFlags = [ + '--web-browser-flag=--no-sandbox', + '--web-browser-flag=--disable-background-timer-throttling', + '--web-browser-flag=--disable-renderer-backgrounding', + '--web-browser-flag=--disable-background-networking', + '--web-browser-flag=--disable-search-engine-choice-screen', + '--web-browser-flag=--disable-extensions', + '--web-browser-flag=--disable-popup-blocking', + '--web-browser-flag=--disable-translate', + '--web-browser-flag=--disable-default-apps', + '--web-browser-flag=--no-default-browser-check', + '--web-browser-flag=--no-first-run', + '--web-browser-flag=--password-store=basic', + '--web-browser-flag=--bwsi', +]; void main() { group('test drive_example_command', () { - late Platform mockPlatform; + late MockPlatform mockPlatform; late Directory packagesDir; late CommandRunner runner; late RecordingProcessRunner processRunner; @@ -776,6 +790,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -823,6 +838,7 @@ void main() { '--web-port=7357', '--browser-name=chrome', '--wasm', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -870,6 +886,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -918,6 +935,56 @@ void main() { '--web-port=7357', '--browser-name=chrome', '--chrome-binary=/path/to/chrome', + ..._defaultWebBrowserFlags, + '--screenshot=/path/to/logs/plugin_example-drive', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/plugin_test.dart', + ], pluginExampleDirectory.path), + ]), + ); + }); + + test('drives a web plugin on macOS with mock keychain', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/integration_test/plugin_test.dart', + 'example/test_driver/integration_test.dart', + 'example/web/index.html', + ], + platformSupport: { + platformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = getExampleDir(plugin); + + mockPlatform.isMacOS = true; + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([contains('Running for plugin'), contains('No issues found!')]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + ..._defaultWebBrowserFlags, + '--web-browser-flag=--use-mock-keychain', '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1370,6 +1437,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1382,6 +1450,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/plugin_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1488,6 +1557,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/a_package_example-drive', '--driver', 'test_driver/integration_test.dart', @@ -1534,6 +1604,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--driver', 'test_driver/integration_test.dart', '--target', @@ -1617,6 +1688,7 @@ void main() { 'web-server', '--web-port=7357', '--browser-name=chrome', + ..._defaultWebBrowserFlags, '--screenshot=/path/to/logs/a_package_example_with_web-drive', '--driver', 'test_driver/integration_test.dart',