blob: 27199320fc567f5b0b8afa82a9f6e165a2cfcb58 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html';
import 'dart:js_util' as js_util;
// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#106316)
// ignore: unnecessary_import
import 'dart:ui';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_web/src/camera.dart';
import 'package:camera_web/src/camera_service.dart';
import 'package:camera_web/src/shims/dart_js_util.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:mocktail/mocktail.dart';
import 'helpers/helpers.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('CameraService', () {
const int cameraId = 1;
late Window window;
late Navigator navigator;
late MediaDevices mediaDevices;
late CameraService cameraService;
late JsUtil jsUtil;
setUp(() async {
window = MockWindow();
navigator = MockNavigator();
mediaDevices = MockMediaDevices();
jsUtil = MockJsUtil();
when(() => window.navigator).thenReturn(navigator);
when(() => navigator.mediaDevices).thenReturn(mediaDevices);
// Mock JsUtil to return the real getProperty from dart:js_util.
when<dynamic>(() => jsUtil.getProperty(any(), any())).thenAnswer(
(Invocation invocation) => js_util.getProperty<dynamic>(
invocation.positionalArguments[0] as Object,
invocation.positionalArguments[1] as Object,
),
);
cameraService = CameraService()..window = window;
});
group('getMediaStreamForOptions', () {
testWidgets(
'calls MediaDevices.getUserMedia '
'with provided options', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenAnswer((_) async => FakeMediaStream(<MediaStreamTrack>[]));
final CameraOptions options = CameraOptions(
video: VideoConstraints(
facingMode: FacingModeConstraint.exact(CameraType.user),
width: const VideoSizeConstraint(ideal: 200),
),
);
await cameraService.getMediaStreamForOptions(options);
verify(
() => mediaDevices.getUserMedia(options.toJson()),
).called(1);
});
testWidgets(
'throws PlatformException '
'with notSupported error '
'when there are no media devices', (WidgetTester tester) async {
when(() => navigator.mediaDevices).thenReturn(null);
expect(
() => cameraService.getMediaStreamForOptions(const CameraOptions()),
throwsA(
isA<PlatformException>().having(
(PlatformException e) => e.code,
'code',
CameraErrorCode.notSupported.toString(),
),
),
);
});
group('throws CameraWebException', () {
testWidgets(
'with notFound error '
'when MediaDevices.getUserMedia throws DomException '
'with NotFoundError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('NotFoundError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.notFound),
),
);
});
testWidgets(
'with notFound error '
'when MediaDevices.getUserMedia throws DomException '
'with DevicesNotFoundError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('DevicesNotFoundError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.notFound),
),
);
});
testWidgets(
'with notReadable error '
'when MediaDevices.getUserMedia throws DomException '
'with NotReadableError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('NotReadableError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.notReadable),
),
);
});
testWidgets(
'with notReadable error '
'when MediaDevices.getUserMedia throws DomException '
'with TrackStartError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('TrackStartError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.notReadable),
),
);
});
testWidgets(
'with overconstrained error '
'when MediaDevices.getUserMedia throws DomException '
'with OverconstrainedError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('OverconstrainedError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.overconstrained),
),
);
});
testWidgets(
'with overconstrained error '
'when MediaDevices.getUserMedia throws DomException '
'with ConstraintNotSatisfiedError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('ConstraintNotSatisfiedError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.overconstrained),
),
);
});
testWidgets(
'with permissionDenied error '
'when MediaDevices.getUserMedia throws DomException '
'with NotAllowedError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('NotAllowedError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.permissionDenied),
),
);
});
testWidgets(
'with permissionDenied error '
'when MediaDevices.getUserMedia throws DomException '
'with PermissionDeniedError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('PermissionDeniedError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.permissionDenied),
),
);
});
testWidgets(
'with type error '
'when MediaDevices.getUserMedia throws DomException '
'with TypeError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('TypeError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.type),
),
);
});
testWidgets(
'with abort error '
'when MediaDevices.getUserMedia throws DomException '
'with AbortError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('AbortError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.abort),
),
);
});
testWidgets(
'with security error '
'when MediaDevices.getUserMedia throws DomException '
'with SecurityError', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('SecurityError'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.security),
),
);
});
testWidgets(
'with unknown error '
'when MediaDevices.getUserMedia throws DomException '
'with an unknown error', (WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any()))
.thenThrow(FakeDomException('Unknown'));
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.unknown),
),
);
});
testWidgets(
'with unknown error '
'when MediaDevices.getUserMedia throws an unknown exception',
(WidgetTester tester) async {
when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception());
expect(
() => cameraService.getMediaStreamForOptions(
const CameraOptions(),
cameraId: cameraId,
),
throwsA(
isA<CameraWebException>()
.having((CameraWebException e) => e.cameraId, 'cameraId',
cameraId)
.having((CameraWebException e) => e.code, 'code',
CameraErrorCode.unknown),
),
);
});
});
});
group('getZoomLevelCapabilityForCamera', () {
late Camera camera;
late List<MediaStreamTrack> videoTracks;
setUp(() {
camera = MockCamera();
videoTracks = <MediaStreamTrack>[
MockMediaStreamTrack(),
MockMediaStreamTrack()
];
when(() => camera.textureId).thenReturn(0);
when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks));
cameraService.jsUtil = jsUtil;
});
testWidgets(
'returns the zoom level capability '
'based on the first video track', (WidgetTester tester) async {
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'zoom': true,
});
when(videoTracks.first.getCapabilities).thenReturn(<dynamic, dynamic>{
'zoom': js_util.jsify(<dynamic, dynamic>{
'min': 100,
'max': 400,
'step': 2,
}),
});
final ZoomLevelCapability zoomLevelCapability =
cameraService.getZoomLevelCapabilityForCamera(camera);
expect(zoomLevelCapability.minimum, equals(100.0));
expect(zoomLevelCapability.maximum, equals(400.0));
expect(zoomLevelCapability.videoTrack, equals(videoTracks.first));
});
group('throws CameraWebException', () {
testWidgets(
'with zoomLevelNotSupported error '
'when there are no media devices', (WidgetTester tester) async {
when(() => navigator.mediaDevices).thenReturn(null);
expect(
() => cameraService.getZoomLevelCapabilityForCamera(camera),
throwsA(
isA<CameraWebException>()
.having(
(CameraWebException e) => e.cameraId,
'cameraId',
camera.textureId,
)
.having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.zoomLevelNotSupported,
),
),
);
});
testWidgets(
'with zoomLevelNotSupported error '
'when the zoom level is not supported '
'in the browser', (WidgetTester tester) async {
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'zoom': false,
});
when(videoTracks.first.getCapabilities).thenReturn(<dynamic, dynamic>{
'zoom': <dynamic, dynamic>{
'min': 100,
'max': 400,
'step': 2,
},
});
expect(
() => cameraService.getZoomLevelCapabilityForCamera(camera),
throwsA(
isA<CameraWebException>()
.having(
(CameraWebException e) => e.cameraId,
'cameraId',
camera.textureId,
)
.having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.zoomLevelNotSupported,
),
),
);
});
testWidgets(
'with zoomLevelNotSupported error '
'when the zoom level is not supported '
'by the camera', (WidgetTester tester) async {
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'zoom': true,
});
when(videoTracks.first.getCapabilities)
.thenReturn(<dynamic, dynamic>{});
expect(
() => cameraService.getZoomLevelCapabilityForCamera(camera),
throwsA(
isA<CameraWebException>()
.having(
(CameraWebException e) => e.cameraId,
'cameraId',
camera.textureId,
)
.having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.zoomLevelNotSupported,
),
),
);
});
testWidgets(
'with notStarted error '
'when the camera stream has not been initialized',
(WidgetTester tester) async {
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'zoom': true,
});
// Create a camera stream with no video tracks.
when(() => camera.stream)
.thenReturn(FakeMediaStream(<MediaStreamTrack>[]));
expect(
() => cameraService.getZoomLevelCapabilityForCamera(camera),
throwsA(
isA<CameraWebException>()
.having(
(CameraWebException e) => e.cameraId,
'cameraId',
camera.textureId,
)
.having(
(CameraWebException e) => e.code,
'code',
CameraErrorCode.notStarted,
),
),
);
});
});
});
group('getFacingModeForVideoTrack', () {
setUp(() {
cameraService.jsUtil = jsUtil;
});
testWidgets(
'throws PlatformException '
'with notSupported error '
'when there are no media devices', (WidgetTester tester) async {
when(() => navigator.mediaDevices).thenReturn(null);
expect(
() =>
cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()),
throwsA(
isA<PlatformException>().having(
(PlatformException e) => e.code,
'code',
CameraErrorCode.notSupported.toString(),
),
),
);
});
testWidgets(
'returns null '
'when the facing mode is not supported', (WidgetTester tester) async {
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'facingMode': false,
});
final String? facingMode =
cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack());
expect(facingMode, isNull);
});
group('when the facing mode is supported', () {
late MediaStreamTrack videoTrack;
setUp(() {
videoTrack = MockMediaStreamTrack();
when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities'))
.thenReturn(true);
when(mediaDevices.getSupportedConstraints)
.thenReturn(<dynamic, dynamic>{
'facingMode': true,
});
});
testWidgets(
'returns an appropriate facing mode '
'based on the video track settings', (WidgetTester tester) async {
when(videoTrack.getSettings)
.thenReturn(<dynamic, dynamic>{'facingMode': 'user'});
final String? facingMode =
cameraService.getFacingModeForVideoTrack(videoTrack);
expect(facingMode, equals('user'));
});
testWidgets(
'returns an appropriate facing mode '
'based on the video track capabilities '
'when the facing mode setting is empty',
(WidgetTester tester) async {
when(videoTrack.getSettings).thenReturn(<dynamic, dynamic>{});
when(videoTrack.getCapabilities).thenReturn(<dynamic, dynamic>{
'facingMode': <dynamic>['environment', 'left']
});
when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities'))
.thenReturn(true);
final String? facingMode =
cameraService.getFacingModeForVideoTrack(videoTrack);
expect(facingMode, equals('environment'));
});
testWidgets(
'returns null '
'when the facing mode setting '
'and capabilities are empty', (WidgetTester tester) async {
when(videoTrack.getSettings).thenReturn(<dynamic, dynamic>{});
when(videoTrack.getCapabilities)
.thenReturn(<dynamic, dynamic>{'facingMode': <dynamic>[]});
final String? facingMode =
cameraService.getFacingModeForVideoTrack(videoTrack);
expect(facingMode, isNull);
});
testWidgets(
'returns null '
'when the facing mode setting is empty and '
'the video track capabilities are not supported',
(WidgetTester tester) async {
when(videoTrack.getSettings).thenReturn(<dynamic, dynamic>{});
when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities'))
.thenReturn(false);
final String? facingMode =
cameraService.getFacingModeForVideoTrack(videoTrack);
expect(facingMode, isNull);
});
});
});
group('mapFacingModeToLensDirection', () {
testWidgets(
'returns front '
'when the facing mode is user', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToLensDirection('user'),
equals(CameraLensDirection.front),
);
});
testWidgets(
'returns back '
'when the facing mode is environment', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToLensDirection('environment'),
equals(CameraLensDirection.back),
);
});
testWidgets(
'returns external '
'when the facing mode is left', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToLensDirection('left'),
equals(CameraLensDirection.external),
);
});
testWidgets(
'returns external '
'when the facing mode is right', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToLensDirection('right'),
equals(CameraLensDirection.external),
);
});
});
group('mapFacingModeToCameraType', () {
testWidgets(
'returns user '
'when the facing mode is user', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToCameraType('user'),
equals(CameraType.user),
);
});
testWidgets(
'returns environment '
'when the facing mode is environment', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToCameraType('environment'),
equals(CameraType.environment),
);
});
testWidgets(
'returns user '
'when the facing mode is left', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToCameraType('left'),
equals(CameraType.user),
);
});
testWidgets(
'returns user '
'when the facing mode is right', (WidgetTester tester) async {
expect(
cameraService.mapFacingModeToCameraType('right'),
equals(CameraType.user),
);
});
});
group('mapResolutionPresetToSize', () {
testWidgets(
'returns 4096x2160 '
'when the resolution preset is max', (WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.max),
equals(const Size(4096, 2160)),
);
});
testWidgets(
'returns 4096x2160 '
'when the resolution preset is ultraHigh',
(WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh),
equals(const Size(4096, 2160)),
);
});
testWidgets(
'returns 1920x1080 '
'when the resolution preset is veryHigh',
(WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.veryHigh),
equals(const Size(1920, 1080)),
);
});
testWidgets(
'returns 1280x720 '
'when the resolution preset is high', (WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.high),
equals(const Size(1280, 720)),
);
});
testWidgets(
'returns 720x480 '
'when the resolution preset is medium', (WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.medium),
equals(const Size(720, 480)),
);
});
testWidgets(
'returns 320x240 '
'when the resolution preset is low', (WidgetTester tester) async {
expect(
cameraService.mapResolutionPresetToSize(ResolutionPreset.low),
equals(const Size(320, 240)),
);
});
});
group('mapDeviceOrientationToOrientationType', () {
testWidgets(
'returns portraitPrimary '
'when the device orientation is portraitUp',
(WidgetTester tester) async {
expect(
cameraService.mapDeviceOrientationToOrientationType(
DeviceOrientation.portraitUp,
),
equals(OrientationType.portraitPrimary),
);
});
testWidgets(
'returns landscapePrimary '
'when the device orientation is landscapeLeft',
(WidgetTester tester) async {
expect(
cameraService.mapDeviceOrientationToOrientationType(
DeviceOrientation.landscapeLeft,
),
equals(OrientationType.landscapePrimary),
);
});
testWidgets(
'returns portraitSecondary '
'when the device orientation is portraitDown',
(WidgetTester tester) async {
expect(
cameraService.mapDeviceOrientationToOrientationType(
DeviceOrientation.portraitDown,
),
equals(OrientationType.portraitSecondary),
);
});
testWidgets(
'returns landscapeSecondary '
'when the device orientation is landscapeRight',
(WidgetTester tester) async {
expect(
cameraService.mapDeviceOrientationToOrientationType(
DeviceOrientation.landscapeRight,
),
equals(OrientationType.landscapeSecondary),
);
});
});
group('mapOrientationTypeToDeviceOrientation', () {
testWidgets(
'returns portraitUp '
'when the orientation type is portraitPrimary',
(WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
OrientationType.portraitPrimary,
),
equals(DeviceOrientation.portraitUp),
);
});
testWidgets(
'returns landscapeLeft '
'when the orientation type is landscapePrimary',
(WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
OrientationType.landscapePrimary,
),
equals(DeviceOrientation.landscapeLeft),
);
});
testWidgets(
'returns portraitDown '
'when the orientation type is portraitSecondary',
(WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
OrientationType.portraitSecondary,
),
equals(DeviceOrientation.portraitDown),
);
});
testWidgets(
'returns portraitDown '
'when the orientation type is portraitSecondary',
(WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
OrientationType.portraitSecondary,
),
equals(DeviceOrientation.portraitDown),
);
});
testWidgets(
'returns landscapeRight '
'when the orientation type is landscapeSecondary',
(WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
OrientationType.landscapeSecondary,
),
equals(DeviceOrientation.landscapeRight),
);
});
testWidgets(
'returns portraitUp '
'for an unknown orientation type', (WidgetTester tester) async {
expect(
cameraService.mapOrientationTypeToDeviceOrientation(
'unknown',
),
equals(DeviceOrientation.portraitUp),
);
});
});
});
}
class JSNoSuchMethodError implements Exception {}