From e2b5b1c737e7ea430a04e6c2026cf1e5e47923a8 Mon Sep 17 00:00:00 2001 From: Sai Krishna Date: Fri, 19 Jul 2024 06:31:28 +0530 Subject: [PATCH] Added support to mock camera and image picker (#22) added support mock camera and image picker for android --- .../android/app/src/debug/AndroidManifest.xml | 5 + .../android/app/src/main/AndroidManifest.xml | 7 + .../app/src/profile/AndroidManifest.xml | 4 + demo-app/lib/screens/home_screen.dart | 6 + demo-app/lib/screens/image_picker.dart | 577 ++++++++++++++++++ demo-app/pubspec.lock | 302 ++++++++- demo-app/pubspec.yaml | 4 + server/lib/src/codexc.dart | 278 +++++++++ server/lib/src/driver.dart | 25 + .../src/handler/activate_inject_image.dart | 23 + server/lib/src/handler/inject_image.dart | 27 + server/lib/src/handler/new_session.dart | 9 + .../src/models/api/activate_inject_image.dart | 12 + server/lib/src/models/api/inject_image.dart | 12 + server/lib/src/runner.dart | 5 +- server/lib/src/server.dart | 4 + server/lib/src/temo | 0 server/lib/src/utils/camera_mocking.dart | 78 +++ server/pubspec.lock | 146 ++++- server/pubspec.yaml | 6 +- 20 files changed, 1515 insertions(+), 15 deletions(-) create mode 100644 demo-app/lib/screens/image_picker.dart create mode 100644 server/lib/src/codexc.dart create mode 100644 server/lib/src/handler/activate_inject_image.dart create mode 100644 server/lib/src/handler/inject_image.dart create mode 100644 server/lib/src/models/api/activate_inject_image.dart create mode 100644 server/lib/src/models/api/inject_image.dart create mode 100644 server/lib/src/temo create mode 100644 server/lib/src/utils/camera_mocking.dart diff --git a/demo-app/android/app/src/debug/AndroidManifest.xml b/demo-app/android/app/src/debug/AndroidManifest.xml index 399f698..e6e5d02 100644 --- a/demo-app/android/app/src/debug/AndroidManifest.xml +++ b/demo-app/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,9 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + + + + diff --git a/demo-app/android/app/src/main/AndroidManifest.xml b/demo-app/android/app/src/main/AndroidManifest.xml index f905ddc..a29925c 100644 --- a/demo-app/android/app/src/main/AndroidManifest.xml +++ b/demo-app/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,11 @@ + + + + + + @@ -9,6 +15,7 @@ + + + + diff --git a/demo-app/lib/screens/home_screen.dart b/demo-app/lib/screens/home_screen.dart index bf98b47..36647ec 100644 --- a/demo-app/lib/screens/home_screen.dart +++ b/demo-app/lib/screens/home_screen.dart @@ -1,5 +1,6 @@ import 'package:appium_testing_app/components/custom_app_bar.dart'; import 'package:appium_testing_app/models/feature_model.dart'; +import 'package:appium_testing_app/screens/image_picker.dart'; import 'package:appium_testing_app/screens/lazy_loading.dart'; import 'package:appium_testing_app/screens/loader_screen.dart'; import 'package:appium_testing_app/screens/native_screen.dart'; @@ -63,6 +64,8 @@ class _HomeScreenState extends State { title: "Loader Screen", subtitle: "Page with loader and a button")); featureModels.add(FeatureModel( title: "Contact permission", subtitle: "Asks for contact permission with native popup")); + featureModels.add(FeatureModel( + title: "Image Picker", subtitle: "Mock Camera Image Picker")); } @override @@ -153,6 +156,9 @@ class _HomeScreenState extends State { case 14: page = ContactPermissionScreen(title: featureModels[index].title,); break; + case 15: + page = ImagePickerScreen(title: featureModels[index].title,); + break; default: page = NativeScreen(title: featureModels[index].title); break; diff --git a/demo-app/lib/screens/image_picker.dart b/demo-app/lib/screens/image_picker.dart new file mode 100644 index 0000000..bce5348 --- /dev/null +++ b/demo-app/lib/screens/image_picker.dart @@ -0,0 +1,577 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; +import 'package:qr_code_dart_scan/qr_code_dart_scan.dart'; +import 'package:video_player/video_player.dart'; + +class ImagePickerScreen extends StatefulWidget { + const ImagePickerScreen({super.key, this.title}); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _mediaFileList; + + void _setImageFileListFromFile(XFile? value) { + _mediaFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + String? _qrCodeValue; + + final ImagePicker _picker = ImagePicker(); + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + final TextEditingController limitController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.networkUrl(Uri.parse(file.path)); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + const double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (context.mounted) { + if (isVideo) { + final XFile? file = await _picker.pickVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context, true, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { + try { + final List pickedFileList = isMedia + ? await _picker.pickMultipleMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + limit: limit, + ) + : await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + limit: limit, + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { + try { + final List pickedFileList = []; + final XFile? media = await _picker.pickMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else { + await _displayPickImageDialog(context, false, (double? maxWidth, + double? maxHeight, int? quality, int? limit) async { + try { + final XFile? pickedFile = await _picker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + final decoder = + QRCodeDartScanDecoder(formats: [BarcodeFormat.qrCode]); + Result? result = await decoder.decodeFile(pickedFile!, scanInverted: true); + _qrCodeValue = result?.text; + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e, stacktrace) { + setState(() { + _pickImageError = e; + }); + } + }); + } + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_mediaFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + + // Why network for web? + // See https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform + return Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(_mediaFileList![index].path) + : (mime == null || mime.startsWith('image/') + ? Column( + children: [ + Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text( + 'This image type is not supported')); + }, + ), + Text(_qrCodeValue ?? 'No QR code found'), + ], + ) + : _buildInlineVideoPlayer(index)), + ); + }, + itemCount: _mediaFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + Future retrieveLostData() async { + final LostDataResponse response = await _picker.retrieveLostData(); + if (response.isEmpty) { + return; + } + if (response.file != null) { + if (response.type == RetrieveType.video) { + isVideo = true; + await _playVideo(response.file); + } else { + isVideo = false; + setState(() { + if (response.files == null) { + _setImageFileListFromFile(response.file); + } else { + _mediaFileList = response.files; + } + }); + } + } else { + _retrieveDataError = response.exception!.code; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title ?? ''), + ), + body: Center( + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android + ? FutureBuilder( + future: retrieveLostData(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + case ConnectionState.done: + return _handlePreview(); + case ConnectionState.active: + if (snapshot.hasError) { + return Text( + 'Pick image/video error: ${snapshot.error}}', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + }, + ) + : _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + key: const ValueKey('pick_image'), + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + key: const ValueKey('capture_image'), + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, bool isMulti, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + if (isMulti) + TextField( + controller: limitController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter limit if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + final int? limit = limitController.text.isNotEmpty + ? int.parse(limitController.text) + : null; + onPick(width, height, quality, limit); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality, int? limit); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {super.key}); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/demo-app/pubspec.lock b/demo-app/pubspec.lock index 493b180..4e37aa3 100644 --- a/demo-app/pubspec.lock +++ b/demo-app/pubspec.lock @@ -15,7 +15,15 @@ packages: path: "../server" relative: true source: path - version: "0.0.18" + version: "0.0.20" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" async: dependency: transitive description: @@ -32,6 +40,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + camera: + dependency: transitive + description: + name: camera + sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 + url: "https://pub.dev" + source: hosted + version: "0.10.6" + camera_android: + dependency: transitive + description: + name: camera_android + sha256: eacc70b6c81fa5e17921302fc17a1ae5983d94ce1d76c71e4869abdc615d35d2 + url: "https://pub.dev" + source: hosted + version: "0.10.9+8" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "95c2f40b4d06cdb0fd2ad893c762d1f2c931a5e370793ec34c939eb4fcbf96bb" + url: "https://pub.dev" + source: hosted + version: "0.9.17" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 + url: "https://pub.dev" + source: hosted + version: "2.8.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: b9235ec0a2ce949daec546f1f3d86f05c3921ed31c7d9ab6b7c03214d152fc2d + url: "https://pub.dev" + source: hosted + version: "0.3.4" carousel_slider: dependency: "direct main" description: @@ -48,6 +96,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charset: + dependency: transitive + description: + name: charset + sha256: "27802032a581e01ac565904ece8c8962564b1070690794f0072f6865958ce8b9" + url: "https://pub.dev" + source: hosted + version: "2.0.1" clock: dependency: transitive description: @@ -72,6 +128,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -80,6 +144,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" cupertino_icons: dependency: "direct main" description: @@ -104,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -128,6 +208,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" flutter: dependency: "direct main" description: flutter @@ -146,6 +258,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + url: "https://pub.dev" + source: hosted + version: "2.0.20" flutter_test: dependency: "direct dev" description: flutter @@ -169,14 +289,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_methods: dependency: transitive description: @@ -193,11 +321,91 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: "direct main" + description: + name: image_picker_android + sha256: ff39a10ab4f48f4ac70776d0494a97bf073cd2570892cd46bc8a5cac162c25db + url: "https://pub.dev" + source: hosted + version: "0.8.12+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" integration_test: dependency: transitive description: flutter source: sdk version: "0.0.0" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: @@ -314,10 +522,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.7" path_provider_foundation: dependency: transitive description: @@ -346,10 +554,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" permission_handler: dependency: "direct main" description: @@ -398,6 +606,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -422,6 +638,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.2" + qr_code_dart_scan: + dependency: "direct main" + description: + name: qr_code_dart_scan + sha256: "52912da40f5e40a197b890108af9d2a6baa0c5812b77bfb085c8ee9e3c4f1f52" + url: "https://pub.dev" + source: hosted + version: "0.8.1" shelf: dependency: transitive description: @@ -575,10 +799,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -627,6 +851,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d + url: "https://pub.dev" + source: hosted + version: "2.9.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: fdc0331ce9f808cc2714014cb8126bd6369943affefd54f8fdab0ea0bb617b7f + url: "https://pub.dev" + source: hosted + version: "2.5.2" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + url: "https://pub.dev" + source: hosted + version: "2.6.1" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" vm_service: dependency: transitive description: @@ -695,10 +959,10 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "7affdf9d680c015b11587181171d3cad8093e449db1f7d9f0f08f4f33d24f9a0" + sha256: "9c62cc46fa4f2d41e10ab81014c1de470a6c6f26051a2de32111b2ee55287feb" url: "https://pub.dev" source: hosted - version: "3.13.1" + version: "3.14.0" win32: dependency: transitive description: @@ -723,6 +987,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + zxing_lib: + dependency: transitive + description: + name: zxing_lib + sha256: "870a63610be3f20009ca9201f7ba2d53d7eaefa675c154b3e8c1f6fc55984d04" + url: "https://pub.dev" + source: hosted + version: "1.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.22.0" diff --git a/demo-app/pubspec.yaml b/demo-app/pubspec.yaml index ed438fd..d1bc852 100644 --- a/demo-app/pubspec.yaml +++ b/demo-app/pubspec.yaml @@ -23,6 +23,10 @@ dependencies: carousel_slider: ^4.2.1 permission_handler: ^11.3.1 url_launcher: ^6.3.0 + qr_code_dart_scan: ^0.8.1 + image_picker: ^1.1.2 + video_player: 2.9.1 + image_picker_android: ^0.8.12+4 flutter: sdk: flutter diff --git a/server/lib/src/codexc.dart b/server/lib/src/codexc.dart new file mode 100644 index 0000000..12d895f --- /dev/null +++ b/server/lib/src/codexc.dart @@ -0,0 +1,278 @@ +import 'package:flutter/services.dart'; + +enum SourceCamera { + rear, + front, +} + +enum SourceType { + camera, + gallery, +} + +enum CacheRetrievalType { + image, + video, +} + +class GeneralOptions { + GeneralOptions({ + required this.allowMultiple, + required this.usePhotoPicker, + this.limit, + }); + + bool allowMultiple; + + bool usePhotoPicker; + + int? limit; + + Object encode() { + return [ + allowMultiple, + usePhotoPicker, + limit, + ]; + } + + static GeneralOptions decode(Object result) { + result as List; + return GeneralOptions( + allowMultiple: result[0]! as bool, + usePhotoPicker: result[1]! as bool, + limit: result[2] as int?, + ); + } +} + +/// Options for image selection and output. +class ImageSelectionOptions { + ImageSelectionOptions({ + this.maxWidth, + this.maxHeight, + required this.quality, + }); + + /// If set, the max width that the image should be resized to fit in. + double? maxWidth; + + /// If set, the max height that the image should be resized to fit in. + double? maxHeight; + + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + int quality; + + Object encode() { + return [ + maxWidth, + maxHeight, + quality, + ]; + } + + static ImageSelectionOptions decode(Object result) { + result as List; + return ImageSelectionOptions( + maxWidth: result[0] as double?, + maxHeight: result[1] as double?, + quality: result[2]! as int, + ); + } +} + +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; + + Object encode() { + return [ + imageSelectionOptions.encode(), + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + imageSelectionOptions: + ImageSelectionOptions.decode(result[0]! as List), + ); + } +} + +/// Options for image selection and output. +class VideoSelectionOptions { + VideoSelectionOptions({ + this.maxDurationSeconds, + }); + + /// The maximum desired length for the video, in seconds. + int? maxDurationSeconds; + + Object encode() { + return [ + maxDurationSeconds, + ]; + } + + static VideoSelectionOptions decode(Object result) { + result as List; + return VideoSelectionOptions( + maxDurationSeconds: result[0] as int?, + ); + } +} + +/// Specification for the source of an image or video selection. +class SourceSpecification { + SourceSpecification({ + required this.type, + this.camera, + }); + + SourceType type; + + SourceCamera? camera; + + Object encode() { + return [ + type.index, + camera?.index, + ]; + } + + static SourceSpecification decode(Object result) { + result as List; + return SourceSpecification( + type: SourceType.values[result[0]! as int], + camera: result[1] != null ? SourceCamera.values[result[1]! as int] : null, + ); + } +} + +/// An error that occurred during lost result retrieval. +/// +/// The data here maps to the `PlatformException` that will be created from it. +class CacheRetrievalError { + CacheRetrievalError({ + required this.code, + this.message, + }); + + String code; + + String? message; + + Object encode() { + return [ + code, + message, + ]; + } + + static CacheRetrievalError decode(Object result) { + result as List; + return CacheRetrievalError( + code: result[0]! as String, + message: result[1] as String?, + ); + } +} + +/// The result of retrieving cached results from a previous run. +class CacheRetrievalResult { + CacheRetrievalResult({ + required this.type, + this.error, + this.paths = const [], + }); + + /// The type of the retrieved data. + CacheRetrievalType type; + + /// The error from the last selection, if any. + CacheRetrievalError? error; + + /// The results from the last selection, if any. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + List paths; + + Object encode() { + return [ + type.index, + error?.encode(), + paths, + ]; + } + + static CacheRetrievalResult decode(Object result) { + result as List; + return CacheRetrievalResult( + type: CacheRetrievalType.values[result[0]! as int], + error: result[1] != null + ? CacheRetrievalError.decode(result[1]! as List) + : null, + paths: (result[2] as List?)!.cast(), + ); + } +} + +class ImagePickerApiCodec extends StandardMessageCodec { + const ImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CacheRetrievalError) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is CacheRetrievalResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is GeneralOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is ImageSelectionOptions) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is MediaSelectionOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CacheRetrievalError.decode(readValue(buffer)!); + case 129: + return CacheRetrievalResult.decode(readValue(buffer)!); + case 130: + return GeneralOptions.decode(readValue(buffer)!); + case 131: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: + return VideoSelectionOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} diff --git a/server/lib/src/driver.dart b/server/lib/src/driver.dart index 6f3550c..8351a74 100644 --- a/server/lib/src/driver.dart +++ b/server/lib/src/driver.dart @@ -11,6 +11,9 @@ class FlutterDriver { late PackageInfo _appInfo; String? _serverVersion; Session? _session; + bool _isCameraMocked = false; + final Map _savedFiles = {}; + String? _activeMockImage; FlutterDriver._(); @@ -21,6 +24,8 @@ class FlutterDriver { IntegrationTestWidgetsFlutterBinding get binding => _binding; PackageInfo get appInfo => _appInfo; String? get serverVersion => _serverVersion; + bool get isCameraMocked => _isCameraMocked; + String? get activeMockImage => _activeMockImage; void initialize( {required WidgetTester tester, @@ -53,4 +58,24 @@ class FlutterDriver { void resetSession() { _session = null; } + + void setCameraMocked(bool value) { + _isCameraMocked = value; + } + + void saveFileInfo(String fileName, String filePath) { + _savedFiles[fileName] = filePath; + } + + String? getFilePath(String fileName) { + return _savedFiles[fileName]; + } + + void setActiveMockImage(String? value) { + _activeMockImage = value; + } + + String? getActiveMockImage() { + return _savedFiles[_activeMockImage]; + } } diff --git a/server/lib/src/handler/activate_inject_image.dart b/server/lib/src/handler/activate_inject_image.dart new file mode 100644 index 0000000..7f18e33 --- /dev/null +++ b/server/lib/src/handler/activate_inject_image.dart @@ -0,0 +1,23 @@ +import 'package:appium_flutter_server/src/handler/request/request_handler.dart'; +import 'package:appium_flutter_server/src/models/api/activate_inject_image.dart'; +import 'package:appium_flutter_server/src/models/api/appium_response.dart'; +import 'package:appium_flutter_server/src/models/api/inject_image.dart'; +import 'package:appium_flutter_server/src/utils/camera_mocking.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shelf_plus/shelf_plus.dart'; + +import '../logger.dart'; + +class ActivateInjectImage extends RequestHandler { + ActivateInjectImage(super.route); + + @override + Future handle(Request request) async { + log('ActivateInjectImageHandler'); + ActivateInjectImageModal activateInjectImageModal = ActivateInjectImageModal.fromJson(await request.body.asJson); + + String activeMockedImage = activateInjectedImage(activateInjectImageModal.imageId.toString()); + + return AppiumResponse(getSessionId(request), activeMockedImage); + } +} diff --git a/server/lib/src/handler/inject_image.dart b/server/lib/src/handler/inject_image.dart new file mode 100644 index 0000000..0aa841c --- /dev/null +++ b/server/lib/src/handler/inject_image.dart @@ -0,0 +1,27 @@ +import 'package:appium_flutter_server/src/driver.dart'; +import 'package:appium_flutter_server/src/handler/request/request_handler.dart'; +import 'package:appium_flutter_server/src/internal/flutter_element.dart'; +import 'package:appium_flutter_server/src/models/api/appium_response.dart'; +import 'package:appium_flutter_server/src/models/api/inject_image.dart'; +import 'package:appium_flutter_server/src/models/api/set_text.dart'; +import 'package:appium_flutter_server/src/utils/camera_mocking.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shelf_plus/shelf_plus.dart'; + +import '../logger.dart'; +import '../utils/element_helper.dart'; + +class InjectImage extends RequestHandler { + InjectImage(super.route); + + @override + Future handle(Request request) async { + log('InjectImageHandler'); + InjectImageModal injectedImageModal = InjectImageModal.fromJson(await request.body.asJson); + String base64Image = injectedImageModal.base64Image.toString(); + + String fileName = await saveImageToDevice(base64Image); + + return AppiumResponse(getSessionId(request), fileName); + } +} diff --git a/server/lib/src/handler/new_session.dart b/server/lib/src/handler/new_session.dart index 92647a1..4f1a77d 100644 --- a/server/lib/src/handler/new_session.dart +++ b/server/lib/src/handler/new_session.dart @@ -2,9 +2,12 @@ import 'package:appium_flutter_server/src/driver.dart'; import 'package:appium_flutter_server/src/exceptions/invalid_argument_exception.dart'; import 'package:appium_flutter_server/src/handler/request/no_session_command_handler.dart'; import 'package:appium_flutter_server/src/handler/request/request_handler.dart'; +import 'package:appium_flutter_server/src/logger.dart'; import 'package:appium_flutter_server/src/models/api/appium_response.dart'; import 'package:appium_flutter_server/src/models/api/create_session.dart'; +import 'package:appium_flutter_server/src/utils/camera_mocking.dart'; import 'package:appium_flutter_server/src/utils/w3c_capabilities.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:shelf_plus/shelf_plus.dart'; class NewSessionHandler extends RequestHandler @@ -24,6 +27,12 @@ class NewSessionHandler extends RequestHandler String sessionId = FlutterDriver.instance.initializeSession(session.capabilities!); + if (session.capabilities?['flutterEnableMockCamera'] ?? false) { + var flutterMockCameraValue = session.capabilities?['flutterEnableMockCamera']; + ImagePickerPlatform.instance = MockImagePicker(); + FlutterDriver.instance.setCameraMocked(true); + log('Camera Instance mocked: $flutterMockCameraValue'); + } return AppiumResponse(sessionId, parsedCaps); } } diff --git a/server/lib/src/models/api/activate_inject_image.dart b/server/lib/src/models/api/activate_inject_image.dart new file mode 100644 index 0000000..4277ebb --- /dev/null +++ b/server/lib/src/models/api/activate_inject_image.dart @@ -0,0 +1,12 @@ +class ActivateInjectImageModal { + dynamic imageId; + + ActivateInjectImageModal({this.imageId}); + + factory ActivateInjectImageModal.fromJson(Map json) => ActivateInjectImageModal( + imageId: json['imageId'], + ); + + Map toJson() => + {'imageId': imageId}; +} diff --git a/server/lib/src/models/api/inject_image.dart b/server/lib/src/models/api/inject_image.dart new file mode 100644 index 0000000..ce2f4aa --- /dev/null +++ b/server/lib/src/models/api/inject_image.dart @@ -0,0 +1,12 @@ +class InjectImageModal { + dynamic base64Image; + + InjectImageModal({this.base64Image}); + + factory InjectImageModal.fromJson(Map json) => InjectImageModal( + base64Image: json['base64Image'], + ); + + Map toJson() => + {'base64Image': base64Image}; +} diff --git a/server/lib/src/runner.dart b/server/lib/src/runner.dart index ac26ef6..aff09b6 100644 --- a/server/lib/src/runner.dart +++ b/server/lib/src/runner.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:appium_flutter_server/src/driver.dart'; import 'package:appium_flutter_server/src/appium_test_bindings.dart'; import 'package:flutter/widgets.dart'; @@ -8,10 +7,12 @@ import 'package:appium_flutter_server/src/server.dart'; import 'package:integration_test/integration_test.dart'; import 'package:package_info_plus/package_info_plus.dart'; + const MAX_TEST_DURATION_SECS = 24 * 60 * 60; // Need a better way to fetch this for automated release, this needs to be updated along with version bump // Can stay for now as it is not a breaking change -const serverVersion = '0.0.20'; +const serverVersion = '0.0.21'; + void initializeTest({Widget? app, Function? callback}) async { IntegrationTestWidgetsFlutterBinding binding = diff --git a/server/lib/src/server.dart b/server/lib/src/server.dart index 38b5704..c3c249f 100644 --- a/server/lib/src/server.dart +++ b/server/lib/src/server.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appium_flutter_server/src/handler/activate_inject_image.dart'; import 'package:appium_flutter_server/src/handler/click.dart'; import 'package:appium_flutter_server/src/handler/double_click.dart'; import 'package:appium_flutter_server/src/handler/delete_session.dart'; @@ -29,6 +30,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:appium_flutter_server/src/handler/clear.dart'; import 'handler/gesture/drag_drop.dart'; +import 'handler/inject_image.dart'; import 'handler/long_press.dart'; import 'package:a_bridge/a_bridge.dart'; @@ -74,6 +76,8 @@ class FlutterServer { _registerPost( DoubleClickHandler("/session//element//double_click")); _registerPost(PressBackHandler("/session//back")); + _registerPost(InjectImage("/session//inject_image")); + _registerPost(ActivateInjectImage("/session//activate_inject_image")); /* Gesture handler */ _registerPost( diff --git a/server/lib/src/temo b/server/lib/src/temo new file mode 100644 index 0000000..e69de29 diff --git a/server/lib/src/utils/camera_mocking.dart b/server/lib/src/utils/camera_mocking.dart new file mode 100644 index 0000000..7e4f664 --- /dev/null +++ b/server/lib/src/utils/camera_mocking.dart @@ -0,0 +1,78 @@ + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:appium_flutter_server/src/driver.dart'; +import 'package:appium_flutter_server/src/logger.dart'; +import 'package:appium_flutter_server/src/utils/test_utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'dart:io'; +import 'package:path/path.dart' as path; +class MockImagePicker extends ImagePickerPlatform { + + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + String? image = FlutterDriver.instance.getActiveMockImage(); + return XFile(image!); + } + Future getLostData() { + return Future.value(LostDataResponse.empty()); + } +} + +String activateInjectedImage(String imageId) { + if (FlutterDriver.instance.isCameraMocked) { + FlutterDriver.instance.setActiveMockImage(imageId); + return FlutterDriver.instance.getActiveMockImage()!; + } else { + throw Exception("Make sure you have enabled the capability 'flutterEnableMockCamera: true' and inject an image before activating"); + } +} + +Future saveImageToDevice(String base64String) async { + if (FlutterDriver.instance.isCameraMocked) { + Directory directory; + if (Platform.isAndroid) { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + print('Running on ${androidInfo.version.sdkInt}'); + PermissionStatus permission; + if (androidInfo.version.sdkInt < 33) { + permission = await Permission.storage.request(); + } else { + permission = await Permission.manageExternalStorage.request(); + } + if (permission.isGranted) { + log('Injected Image will be saved in path ${await getDownloadsDirectory()}'); + directory = (await getDownloadsDirectory())!; + } else { + throw Exception("Storage permission not granted"); + } + } else if (Platform.isIOS) { + log('Injected Image will be saved in path ${await getApplicationDocumentsDirectory()}'); + directory = await getApplicationDocumentsDirectory(); + } else { + throw UnsupportedError("Unsupported platform"); + } + String fileName = "${generateUUID()}.png"; + String filePath = path.join(directory.path, fileName); + Uint8List bytes; + bytes = const Base64Decoder().convert(base64String); + + + File file = File(filePath); + await file.writeAsBytes(bytes); + log('File saved to $filePath'); + FlutterDriver.instance.saveFileInfo(fileName, filePath); + FlutterDriver.instance.setActiveMockImage(fileName); + return fileName; + } else { + throw Exception("Make sure you have enabled the capability 'flutterEnableMockCamera: true'"); + } +} diff --git a/server/pubspec.lock b/server/pubspec.lock index 751a5ec..ef488cf 100644 --- a/server/pubspec.lock +++ b/server/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -217,6 +225,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" fixnum: dependency: transitive description: @@ -243,6 +283,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + url: "https://pub.dev" + source: hosted + version: "2.0.20" flutter_test: dependency: "direct main" description: flutter @@ -322,6 +370,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: ff39a10ab4f48f4ac70776d0494a97bf073cd2570892cd46bc8a5cac162c25db + url: "https://pub.dev" + source: hosted + version: "0.8.12+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" integration_test: dependency: "direct main" description: flutter @@ -431,6 +543,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" package_config: dependency: transitive description: @@ -456,7 +576,7 @@ packages: source: hosted version: "3.0.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" @@ -511,6 +631,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "4356882e9abf51aa0d56e8fb886e0d6162719f2310dd71f0b8fa7f34908b128d" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" platform: dependency: transitive description: @@ -612,6 +748,14 @@ packages: description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" source_span: dependency: transitive description: diff --git a/server/pubspec.yaml b/server/pubspec.yaml index 1b7d8f0..356ceef 100644 --- a/server/pubspec.yaml +++ b/server/pubspec.yaml @@ -1,6 +1,6 @@ name: appium_flutter_server description: "Appium Flutter server using Integration Test package for testing Flutter apps with Appium" -version: 0.0.20 +version: 0.0.21 homepage: "https://github.com/AppiumTestDistribution/appium-flutter-server" environment: @@ -23,10 +23,14 @@ dependencies: package_info_plus: any a_bridge: ^0.0.2 device_info_plus: any + image_picker: ^1.1.2 + path: 1.9.0 + permission_handler: ^8.1.6 dev_dependencies: build_runner: ^2.4.10 flutter_lints: ^4.0.0 + mockito: any integration_test: sdk: flutter