Commit a021685f by Victor Sanni Committed by GitHub

Support web implementation of permission_handler (#1121)

* added integration tests

* added basic integration tests

* updated integration tests

* continued integration tests

* added unit test file

* Move each permission request to its own private method

* Add unit tests and fake web permission handler plugin

* added unit tests for camera and microphone

* Updated documentation files

* Create superclass web_handler.dart

* Use OOP delegation pattern for web_handler

* Change web_handler.dart to web_delegate.dart

* Make WebDelegate class initialization lazy

* Create unit test for each method and permission

* removed redundant integration_test module

* remove redundant test_driver module

* Revert unneeded files and add api docs

* Add geolocation to web permission_handler

* Formatted web_delegate.dart

* Avoid swallowing request permission exception

* Replace UnimplementedError with UnsupportedError for requestPermissions method

Co-authored-by: Maurits van Beusekom <maurits@baseflow.com>

* Make WebDelegate instance variables private

---------

Co-authored-by: Victor Sanni <victorsanni@google.com>
Co-authored-by: Maurits van Beusekom <maurits@baseflow.com>
parent 4026813d
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
.project .project
.svn/ .svn/
bin/ bin/
migrate_working_dir/
# IntelliJ related # IntelliJ related
*.iml *.iml
...@@ -38,6 +39,7 @@ android/.settings/ ...@@ -38,6 +39,7 @@ android/.settings/
version version
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
/pubspec.lock
**/doc/api/ **/doc/api/
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
......
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 796c8ef79279f9c774545b3771238c3098dbefab
channel: stable
project_type: package
## 0.1.0
* Adds an initial implementation of Web support for the permission_handler plugin with camera, notifications, and microphone permissions available.
MIT License
Copyright (c) 2018 Baseflow
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
# permission_handler_web
[![pub package](https://img.shields.io/pub/v/permission_handler_web.svg)](https://pub.dartlang.org/packages/permission_handler_web) ![Build status](https://github.com/Baseflow/flutter-permission-handler/workflows/permission_handler_web/badge.svg?branch=master) [![style: flutter lints](https://img.shields.io/badge/style-flutter_lints-40c4ff.svg)](https://pub.dev/packages/flutter_lints)
The official web implementation of the [permission_handler](https://pub.dev/packages/permission_handler) plugin by [Baseflow](https://baseflow.com).
## Usage
This is the officially endorsed web implementation of the permission_handler plugin. This means it will automatically be added to your dependencies when you depend on `permission_handler` in your applications pubspec.yaml.
More detailed instructions on using the API can be found in the [README.md](../permission_handler/README.md) of the [permission_handler](https://pub.dev/packages/permission_handler) package.
## Issues
Please file any issues, bugs or feature requests as an issue on our [GitHub](https://github.com/Baseflow/flutter-permission-handler/issues) page. Commercial support is available, you can contact us at <hello@baseflow.com>.
## Want to contribute
If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](../CONTRIBUTING.md) and send us your [pull request](https://github.com/Baseflow/flutter-permission-handler/pulls).
## Author
This permission_handler plugin for Flutter is developed by [Baseflow](https://baseflow.com).
\ No newline at end of file
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 796c8ef79279f9c774545b3771238c3098dbefab
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: android
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: ios
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: linux
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: macos
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: web
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
- platform: windows
create_revision: 796c8ef79279f9c774545b3771238c3098dbefab
base_revision: 796c8ef79279f9c774545b3771238c3098dbefab
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
import 'package:baseflow_plugin_template/baseflow_plugin_template.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
void main() {
runApp(BaseflowPluginExample(
pluginName: 'Permission Handler',
githubURL: 'https://github.com/Baseflow/flutter-permission-handler',
pubDevURL: 'https://pub.dev/packages/permission_handler',
pages: [PermissionHandlerWidget.createPage()]));
}
///Defines the main theme color
final MaterialColor themeMaterialColor =
BaseflowPluginExample.createMaterialColor(
const Color.fromRGBO(48, 49, 60, 1));
/// A Flutter application demonstrating the functionality of this plugin
class PermissionHandlerWidget extends StatefulWidget {
/// Create a page containing the functionality of this plugin
static ExamplePage createPage() {
return ExamplePage(
Icons.location_on, (context) => PermissionHandlerWidget());
}
@override
_PermissionHandlerWidgetState createState() =>
_PermissionHandlerWidgetState();
}
class _PermissionHandlerWidgetState extends State<PermissionHandlerWidget> {
@override
Widget build(BuildContext context) {
return Center(
child: ListView(
children: Permission.values
.where((permission) {
return permission != Permission.unknown &&
permission != Permission.mediaLibrary &&
permission != Permission.photos &&
permission != Permission.photosAddOnly &&
permission != Permission.reminders &&
permission != Permission.appTrackingTransparency &&
permission != Permission.criticalAlerts;
})
.map((permission) => PermissionWidget(permission))
.toList()),
);
}
}
/// Permission widget containing information about the passed [Permission]
class PermissionWidget extends StatefulWidget {
/// Constructs a [PermissionWidget] for the supplied [Permission]
const PermissionWidget(this._permission);
final Permission _permission;
@override
_PermissionState createState() => _PermissionState(_permission);
}
class _PermissionState extends State<PermissionWidget> {
_PermissionState(this._permission);
final Permission _permission;
final PermissionHandlerPlatform _permissionHandler =
PermissionHandlerPlatform.instance;
PermissionStatus _permissionStatus = PermissionStatus.denied;
@override
void initState() {
super.initState();
_listenForPermissionStatus();
}
void _listenForPermissionStatus() async {
final status = await _permissionHandler.checkPermissionStatus(_permission);
setState(() => _permissionStatus = status);
}
Color getPermissionColor() {
switch (_permissionStatus) {
case PermissionStatus.denied:
return Colors.red;
case PermissionStatus.granted:
return Colors.green;
case PermissionStatus.limited:
return Colors.orange;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
_permission.toString(),
style: Theme.of(context).textTheme.bodyLarge,
),
subtitle: Text(
_permissionStatus.toString(),
style: TextStyle(color: getPermissionColor()),
),
trailing: (_permission is PermissionWithService)
? IconButton(
icon: const Icon(
Icons.info,
color: Colors.white,
),
onPressed: () {
checkServiceStatus(
context, _permission as PermissionWithService);
})
: null,
onTap: () {
requestPermission(_permission);
},
);
}
void checkServiceStatus(
BuildContext context, PermissionWithService permission) async {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
(await _permissionHandler.checkServiceStatus(permission)).toString()),
));
}
Future<void> requestPermission(Permission permission) async {
final status = await _permissionHandler.requestPermissions([permission]);
setState(() {
print(status);
_permissionStatus = status[permission] ?? PermissionStatus.denied;
print(_permissionStatus);
});
}
}
name: permission_handler_web_example
description: Demonstrates how to use the permission_handler_web plugin..
environment:
sdk: '>=3.0.5 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
baseflow_plugin_template: ^2.1.2
flutter:
sdk: flutter
meta: ^1.3.0
permission_handler_platform_interface: any
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
permission_handler_web:
# When depending on this package from a real application you should use:
# permission_handler_web: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
url_launcher: ^6.0.12
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
assets:
- res/images/baseflow_logo_def_light-02.png
- res/images/poweredByBaseflowLogoLight@3x.png
- res/images/logo.png
- res/images/poweredByBaseflow.png
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
This diff was suppressed by a .gitattributes entry.
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>example</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine().then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body>
</html>
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
import 'dart:html' as html;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
import 'package:permission_handler_web/web_delegate.dart';
/// Platform implementation of the permission_handler Flutter plugin.
class WebPermissionHandler extends PermissionHandlerPlatform {
static final html.MediaDevices _devices = html.window.navigator.mediaDevices!;
static final html.Geolocation _geolocation =
html.window.navigator.geolocation;
static final html.Permissions? _htmlPermissions =
html.window.navigator.permissions;
final WebDelegate _webDelegate;
/// Registers the web plugin implementation.
static void registerWith(Registrar registrar) {
PermissionHandlerPlatform.instance = WebPermissionHandler(
webDelegate: WebDelegate(
_devices,
_geolocation,
_htmlPermissions,
),
);
}
/// Constructs a WebPermissionHandler.
WebPermissionHandler({
required WebDelegate webDelegate,
}) : _webDelegate = webDelegate;
@override
Future<Map<Permission, PermissionStatus>> requestPermissions(
List<Permission> permissions) async {
return _webDelegate.requestPermissions(permissions);
}
@override
Future<PermissionStatus> checkPermissionStatus(Permission permission) async {
return _webDelegate.checkPermissionStatus(permission);
}
@override
Future<ServiceStatus> checkServiceStatus(Permission permission) async {
return _webDelegate.checkServiceStatus(permission);
}
@override
Future<bool> shouldShowRequestPermissionRationale(
Permission permission) async {
return SynchronousFuture(false);
}
@override
Future<bool> openAppSettings() {
return SynchronousFuture(false);
}
}
import 'dart:html' as html;
import 'dart:async';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
/// The delegate class for WebPermissionHandler.
/// Used for dependency injection of html.MediaDevices and html.Permissions objects
class WebDelegate {
/// Constructs a WebDelegate.
WebDelegate(
html.MediaDevices? devices,
html.Geolocation? geolocation,
html.Permissions? permissions,
) : _devices = devices,
_geolocation = geolocation,
_htmlPermissions = permissions;
/// The html media devices object used to request camera and microphone permissions.
final html.MediaDevices? _devices;
/// The html geolocation object used to request location permission.
final html.Geolocation? _geolocation;
/// The html permissions object used to check permission status.
final html.Permissions? _htmlPermissions;
/// The permission name to request access to the camera.
static const _microphonePermissionName = 'microphone';
/// The permission name to request access to the camera.
static const _cameraPermissionName = 'camera';
/// The permission name to request notifications from the user.
static const _notificationsPermissionName = 'notifications';
/// The permission name to request access to the user's location.
static const _locationPermissionName = 'location';
/// The status indicates that permission has been granted by the user.
static const _grantedPermissionStatus = 'granted';
/// The status indicates that permission has been denied by the user.
static const _deniedPermissionStatus = 'denied';
/// The status indicates that permission can be requested.
static const _promptPermissionStatus = 'prompt';
PermissionStatus _toPermissionStatus(String? webPermissionStatus) {
switch (webPermissionStatus) {
case _grantedPermissionStatus:
return PermissionStatus.granted;
case _deniedPermissionStatus:
return PermissionStatus.permanentlyDenied;
case _promptPermissionStatus:
default:
return PermissionStatus.denied;
}
}
Future<PermissionStatus> _permissionStatusState(
String webPermissionName, html.Permissions? permissions) async {
final webPermissionStatus =
await permissions?.query({'name': webPermissionName});
return _toPermissionStatus(webPermissionStatus?.state);
}
Future<bool> _requestMicrophonePermission(html.MediaDevices devices) async {
html.MediaStream? mediaStream;
try {
mediaStream = await devices.getUserMedia({'audio': true});
// In browsers, calling [getUserMedia] will start the recording
// automatically right after. This is undesired behavior as
// [requestPermission] is expected to request permission only.
//
// The manual stop action is then needed here for to stop the automatic
// recording.
if (mediaStream.active!) {
final audioTracks = mediaStream.getAudioTracks();
if (audioTracks.isNotEmpty) {
audioTracks[0].stop();
}
}
} on html.DomException {
return false;
}
return true;
}
Future<bool> _requestCameraPermission(html.MediaDevices devices) async {
html.MediaStream? mediaStream;
try {
mediaStream = await devices.getUserMedia({'video': true});
// In browsers, calling [getUserMedia] will start the recording
// automatically right after. This is undesired behavior as
// [requestPermission] is expected to request permission only.
//
// The manual stop action is then needed here for to stop the automatic
// recording.
if (mediaStream.active!) {
final videoTracks = mediaStream.getVideoTracks();
if (videoTracks.isNotEmpty) {
videoTracks[0].stop();
}
}
} on html.DomException {
return false;
}
return true;
}
Future<bool> _requestNotificationPermission() async {
bool granted = false;
html.Notification.requestPermission().then((permission) => {
if (permission == "granted") {granted = true}
});
return granted;
}
Future<bool> _requestLocationPermission(html.Geolocation geolocation) async {
try {
await geolocation.getCurrentPosition();
return true;
} on html.PositionError {
return false;
}
}
Future<PermissionStatus> _requestSingularPermission(
Permission permission) async {
bool permissionGranted = false;
switch (permission) {
case Permission.microphone:
permissionGranted = await _requestMicrophonePermission(_devices!);
break;
case Permission.camera:
permissionGranted = await _requestCameraPermission(_devices!);
break;
case Permission.notification:
permissionGranted = await _requestNotificationPermission();
break;
case Permission.location:
permissionGranted = await _requestLocationPermission(_geolocation!);
break;
default:
throw UnsupportedError(
'The ${permission.toString()} permission is currently not supported on web.',
);
}
if (!permissionGranted) {
return PermissionStatus.permanentlyDenied;
}
return PermissionStatus.granted;
}
/// Requests the user for access to the supplied list of [Permission]s, if
/// they have not already been granted before.
///
/// Returns a [Map] containing the status per requested [Permission].
Future<Map<Permission, PermissionStatus>> requestPermissions(
List<Permission> permissions) async {
final Map<Permission, PermissionStatus> permissionStatusMap = {};
for (final permission in permissions) {
try {
permissionStatusMap[permission] =
await _requestSingularPermission(permission);
} on UnimplementedError {
rethrow;
}
}
return permissionStatusMap;
}
/// Checks the current status of the given [Permission].
Future<PermissionStatus> checkPermissionStatus(Permission permission) async {
String webPermissionName;
switch (permission) {
case Permission.microphone:
webPermissionName = _microphonePermissionName;
break;
case Permission.camera:
webPermissionName = _cameraPermissionName;
break;
case Permission.notification:
webPermissionName = _notificationsPermissionName;
break;
case Permission.location:
webPermissionName = _locationPermissionName;
break;
default:
throw UnimplementedError(
'checkPermissionStatus() has not been implemented for ${permission.toString()} '
'on web.',
);
}
return _permissionStatusState(webPermissionName, _htmlPermissions);
}
/// Checks the current status of the service associated with the given
/// [Permission].
Future<ServiceStatus> checkServiceStatus(Permission permission) async {
try {
final permissionStatus = await checkPermissionStatus(permission);
switch (permissionStatus) {
case PermissionStatus.granted:
return ServiceStatus.enabled;
default:
return ServiceStatus.disabled;
}
} on UnimplementedError {
rethrow;
}
}
}
name: permission_handler_web
description: A new Flutter package project.
version: 0.0.1
homepage:
environment:
sdk: '>=3.0.5 <4.0.0'
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
permission_handler_platform_interface: ^3.7.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
mockito: ^5.4.2
build_runner: ^2.1.2
test: ^1.24.4
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
plugin:
implements: permission_handler
platforms:
web:
pluginClass: WebPermissionHandler
fileName: permission_handler_web.dart
import 'dart:html' as html;
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
import 'package:permission_handler_web/web_delegate.dart';
import 'permission_handler_web_test.mocks.dart';
@GenerateMocks([
html.DomException,
html.Geolocation,
html.Geoposition,
html.MediaDevices,
html.MediaStream,
html.Navigator,
html.Permissions,
html.PermissionStatus,
html.PositionError,
html.Window,
])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final MockDomException domException = MockDomException();
final MockGeolocation geolocation = MockGeolocation();
final MockGeoposition geoposition = MockGeoposition();
final MockMediaDevices mediaDevices = MockMediaDevices();
final MockMediaStream mediaStream = MockMediaStream();
final MockNavigator navigator = MockNavigator();
final MockPermissions permissions = MockPermissions();
final MockPermissionStatus permissionStatus = MockPermissionStatus();
final MockPositionError positionError = MockPositionError();
final MockWindow window = MockWindow();
final WebDelegate fakePlugin =
WebDelegate(mediaDevices, geolocation, permissions);
final List<Permission> testPermissions = [
Permission.camera,
Permission.contacts,
Permission.location,
Permission.microphone,
Permission.notification,
];
when(window.navigator).thenReturn(navigator);
when(navigator.mediaDevices).thenReturn(mediaDevices);
when(navigator.geolocation).thenReturn(geolocation);
when(permissions.query(any)).thenAnswer((_) async => permissionStatus);
// camera stubs
when(mediaStream.active).thenReturn(true);
when(mediaStream.getVideoTracks()).thenReturn(List.empty());
// microphone stubs
when(mediaDevices.getUserMedia({'audio': true}))
.thenAnswer((_) async => mediaStream);
when(mediaStream.getAudioTracks()).thenReturn(List.empty());
// location stubs
when(geolocation.getCurrentPosition()).thenAnswer((_) async => geoposition);
test(
'check permission works for camera before user is prompted for permission',
() async {
when(permissionStatus.state).thenReturn('prompt');
// check permissions status before requesting
final preActualStatus =
await fakePlugin.checkPermissionStatus(Permission.camera);
expect(preActualStatus, PermissionStatus.denied);
});
test('check permission works for camera after user accepts permission',
() async {
when(permissionStatus.state).thenReturn('granted');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.camera);
expect(postActualStatus, PermissionStatus.granted);
});
test('check permission works for camera after user declines permission',
() async {
when(permissionStatus.state).thenReturn('denied');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.camera);
expect(postActualStatus, PermissionStatus.permanentlyDenied);
});
test('request permission works for camera if user grants permission',
() async {
when(mediaDevices.getUserMedia({'video': true}))
.thenAnswer((_) async => mediaStream);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(permissionMap[Permission.camera], PermissionStatus.granted);
});
test('request permission works for camera if user does not grant permission',
() async {
// stubs
when(mediaDevices.getUserMedia({'video': true})).thenThrow(domException);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(
permissionMap[Permission.camera], PermissionStatus.permanentlyDenied);
});
test(
'check permission works for microphone before user is prompted for permission',
() async {
when(permissionStatus.state).thenReturn('prompt');
// check permissions status before requesting
final preActualStatus =
await fakePlugin.checkPermissionStatus(Permission.microphone);
expect(preActualStatus, PermissionStatus.denied);
});
test('check permission works for microphone after user accepts permission',
() async {
when(permissionStatus.state).thenReturn('granted');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.microphone);
expect(postActualStatus, PermissionStatus.granted);
});
test('check permission works for microphone after user declines permission',
() async {
when(permissionStatus.state).thenReturn('denied');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.microphone);
expect(postActualStatus, PermissionStatus.permanentlyDenied);
});
test('request permission works for microphone if user grants permission',
() async {
when(mediaDevices.getUserMedia({'audio': true}))
.thenAnswer((_) async => mediaStream);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(permissionMap[Permission.microphone], PermissionStatus.granted);
});
test(
'request permission works for microphone if user does not grant permission',
() async {
// stubs
when(mediaDevices.getUserMedia({'audio': true})).thenThrow(domException);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(permissionMap[Permission.microphone],
PermissionStatus.permanentlyDenied);
});
test(
'check permission works for notification before user is prompted for permission',
() async {
when(permissionStatus.state).thenReturn('prompt');
// check permissions status before requesting
final preActualStatus =
await fakePlugin.checkPermissionStatus(Permission.notification);
expect(preActualStatus, PermissionStatus.denied);
});
test('check permission works for notification after user accepts permission',
() async {
when(permissionStatus.state).thenReturn('granted');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.notification);
expect(postActualStatus, PermissionStatus.granted);
});
test('check permission works for notification after user declines permission',
() async {
when(permissionStatus.state).thenReturn('denied');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.notification);
expect(postActualStatus, PermissionStatus.permanentlyDenied);
});
test(
'check permission works for location before user is prompted for permission',
() async {
when(permissionStatus.state).thenReturn('prompt');
// check permissions status before requesting
final preActualStatus =
await fakePlugin.checkPermissionStatus(Permission.location);
expect(preActualStatus, PermissionStatus.denied);
});
test('check permission works for location after user accepts permission',
() async {
when(permissionStatus.state).thenReturn('granted');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.location);
expect(postActualStatus, PermissionStatus.granted);
});
test('check permission works for location after user declines permission',
() async {
when(permissionStatus.state).thenReturn('denied');
// check permissions status after requesting
final postActualStatus =
await fakePlugin.checkPermissionStatus(Permission.location);
expect(postActualStatus, PermissionStatus.permanentlyDenied);
});
test('request permission works for location if user grants permission',
() async {
when(geolocation.getCurrentPosition()).thenAnswer((_) async => geoposition);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(permissionMap[Permission.location], PermissionStatus.granted);
});
test(
'request permission works for location if user does not grant permission',
() async {
// stubs
when(geolocation.getCurrentPosition()).thenThrow(positionError);
// request permission
final permissionMap = await fakePlugin.requestPermissions(testPermissions);
expect(
permissionMap[Permission.location], PermissionStatus.permanentlyDenied);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment