From 9c078e442030a1e1712bbd312c46659aedf5c64a Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 15 May 2020 15:26:29 -0700 Subject: [PATCH 1/9] Add image_picker_for_web plugin. --- .../image_picker_for_web/CHANGELOG.md | 3 + .../image_picker/image_picker_for_web/LICENSE | 27 ++++ .../image_picker_for_web/README.md | 25 +++ .../image_picker_for_web/android/.gitignore | 8 + .../image_picker_for_web/android/build.gradle | 33 ++++ .../android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../ImagePickerWebPlugin.java | 28 ++++ .../ios/image_picker_for_web.podspec | 20 +++ .../lib/image_picker_for_web.dart | 144 ++++++++++++++++++ .../image_picker_for_web/pubspec.yaml | 34 +++++ .../test/url_launcher_web_test.dart | 103 +++++++++++++ 14 files changed, 436 insertions(+) create mode 100644 packages/image_picker/image_picker_for_web/CHANGELOG.md create mode 100644 packages/image_picker/image_picker_for_web/LICENSE create mode 100644 packages/image_picker/image_picker_for_web/README.md create mode 100644 packages/image_picker/image_picker_for_web/android/.gitignore create mode 100644 packages/image_picker/image_picker_for_web/android/build.gradle create mode 100644 packages/image_picker/image_picker_for_web/android/gradle.properties create mode 100644 packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packages/image_picker/image_picker_for_web/android/settings.gradle create mode 100644 packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml create mode 100644 packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java create mode 100644 packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec create mode 100644 packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart create mode 100644 packages/image_picker/image_picker_for_web/pubspec.yaml create mode 100644 packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md new file mode 100644 index 000000000000..18ff7e526b11 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +* Initial open-source release. diff --git a/packages/image_picker/image_picker_for_web/LICENSE b/packages/image_picker/image_picker_for_web/LICENSE new file mode 100644 index 000000000000..0c382ce171cc --- /dev/null +++ b/packages/image_picker/image_picker_for_web/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md new file mode 100644 index 000000000000..3582b604f637 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/README.md @@ -0,0 +1,25 @@ +# image_picker_for_web + +The web implementation of [`image_picker`][1]. + +## Usage + +### Import the package + +This package is the endorsed implementation of `image_picker` for the web platform since version `0.6.7`, so it gets automatically added to your dependencies by depending on `image_picker: ^0.6.7`. + +No modifications to your pubspec.yaml should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): + +```yaml +... +dependencies: + ... + image_picker: ^0.6.7 + ... +... +``` + +### Use the plugin +You should be able to use `package:image_picker` as normal. + +[1]: ../image_picker/image_picker diff --git a/packages/image_picker/image_picker_for_web/android/.gitignore b/packages/image_picker/image_picker_for_web/android/.gitignore new file mode 100644 index 000000000000..c6cbe562a427 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/packages/image_picker/image_picker_for_web/android/build.gradle b/packages/image_picker/image_picker_for_web/android/build.gradle new file mode 100644 index 000000000000..6d8d50eb7b6d --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/build.gradle @@ -0,0 +1,33 @@ +group 'io.flutter.image_picker_for_web' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } +} diff --git a/packages/image_picker/image_picker_for_web/android/gradle.properties b/packages/image_picker/image_picker_for_web/android/gradle.properties new file mode 100644 index 000000000000..7be3d8b46841 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true diff --git a/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..019065d1d650 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/image_picker/image_picker_for_web/android/settings.gradle b/packages/image_picker/image_picker_for_web/android/settings.gradle new file mode 100644 index 000000000000..07e3728d1fe7 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'image_picker_for_web' diff --git a/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b6f6992b3fb9 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java new file mode 100644 index 000000000000..18b5bf21144b --- /dev/null +++ b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java @@ -0,0 +1,28 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.image_picker_for_web; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.PluginRegistry.Registrar; + +/** ImagePickerWebPlugin */ +public class ImagePickerWebPlugin implements FlutterPlugin { + @Override + public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} + + // This static function is optional and equivalent to onAttachedToEngine. It supports the old + // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting + // plugin registration via this function while apps migrate to use the new Android APIs + // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. + // + // It is encouraged to share logic between onAttachedToEngine and registerWith to keep + // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called + // depending on the user's project. onAttachedToEngine or registerWith must both be defined + // in the same class. + public static void registerWith(Registrar registrar) {} + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) {} +} diff --git a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec new file mode 100644 index 000000000000..23fb795d1cc2 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec @@ -0,0 +1,20 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'image_picker_for_web' + s.version = '0.0.1' + s.summary = 'No-op implementation of image_picker_for_web plugin to avoid build issues on iOS' + s.description = <<-DESC +temp fake image_picker_for_web plugin + DESC + s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web' + s.license = { :file => '../LICENSE' } + s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart new file mode 100644 index 000000000000..cfaf814c65c1 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:meta/meta.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; +final String _kAcceptImageMimeType = 'image/*'; +final String _kAcceptVideoMimeType = 'video/*'; + +/// The web implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for the web. +class ImagePickerPlugin extends ImagePickerPlatform { + html.Element _target; + + /// A constructor that allows tests to override the window object used by the plugin. + ImagePickerPlugin({@visibleForTesting html.Element target}) + : _target = target { + if (_target == null) { + _target = _initTarget(_kImagePickerInputsDomId); + } + } + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith(Registrar registrar) { + ImagePickerPlatform.instance = ImagePickerPlugin(); + } + + @override + Future pickImage({ + @required ImageSource source, + double maxWidth, + double maxHeight, + int imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + String capture = _computeCaptureAttribute(source, preferredCameraDevice); + return _pickFile(accept: _kAcceptImageMimeType, capture: capture); + } + + @override + Future pickVideo({ + @required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration maxDuration, + }) { + String capture = _computeCaptureAttribute(source, preferredCameraDevice); + return _pickFile(accept: _kAcceptVideoMimeType, capture: capture); + } + + /// Injects a file input with the specified accept+capture attributes, and + /// returns the PickedFile that the user selected locally. + /// + /// `capture` is only supported in mobile browsers. + /// See https://caniuse.com/#feat=html-media-capture + Future _pickFile({ + String accept, + String capture, + }) { + html.FileUploadInputElement input = _createInputElement(accept, capture); + _injectAndActivate(input); + return _getSelectedFile(input); + } + + // DOM methods + + /// Converts plugin configuration into a proper value for the `capture` attribute. + String _computeCaptureAttribute(ImageSource source, CameraDevice device) { + String capture; + if (source == ImageSource.camera) { + capture = device == CameraDevice.front ? 'user' : 'environment'; + } + return capture; + } + + /// Handles the OnChange event from a FileUploadInputElement object + /// Returns the objectURL of the selected file. + String _handleOnChangeEvent(html.Event event) { + // load the file... + final html.FileUploadInputElement input = event.target; + final html.File file = input.files[0]; + + if (file != null) { + return html.Url.createObjectUrl(file); + } + return null; + } + + /// Monitors an and returns the selected file. + Future _getSelectedFile(html.FileUploadInputElement input) async { + // Observe the input until we can return something + final Completer _completer = Completer(); + input.onChange.listen((html.Event event) async { + final objectUrl = _handleOnChangeEvent(event); + _completer.complete(PickedFile(objectUrl)); + }); + input.onError // What other events signal failure? + .listen((html.Event event) { + _completer.completeError(event); + }); + + return _completer.future; + } + + /// Initializes a DOM container where we can host input elements. + html.Element _initTarget(String id) { + var target = html.querySelector('#${id}'); + if (target == null) { + final html.Element targetElement = + html.Element.tag('flt-image-picker-inputs')..id = id; + + html.querySelector('body').children.add(targetElement); + target = targetElement; + } + return target; + } + + /// Creates an input element that accepts certain file types, and + /// allows to `capture` from the device's cameras (where supported) + html.Element _createInputElement(String accept, String capture) { + html.Element element; + + if (capture != null) { + // Capture is not supported by dart:html :/ + element = html.Element.html( + '', + validator: html.NodeValidatorBuilder() + ..allowElement('input', attributes: ['type', 'accept', 'capture'])); + } else { + element = html.FileUploadInputElement()..accept = accept; + } + + return element; + } + + /// Injects the file input element, and clicks on it + void _injectAndActivate(html.Element element) { + _target.children.clear(); + _target.children.add(element); + element.click(); + } +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml new file mode 100644 index 000000000000..1ec71443bc21 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -0,0 +1,34 @@ +name: image_picker_for_web +description: Web platform implementation of image_picker +homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web +# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump +# the version to 2.0.0. +# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +version: 0.1.0 + +flutter: + plugin: + platforms: + web: + pluginClass: ImagePickerPlugin + fileName: image_picker_for_web.dart + +dependencies: + image_picker_platform_interface: + path: ../image_picker_platform_interface + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.1.7 + js: ^0.6.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.8.0 + mockito: ^4.1.1 + +environment: + sdk: ">=2.5.0 <3.0.0" + flutter: ">=1.10.0 <2.0.0" diff --git a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart new file mode 100644 index 000000000000..b6cf8b70460e --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart @@ -0,0 +1,103 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; +import 'package:url_launcher_web/src/navigator.dart' as navigator; +import 'package:mockito/mockito.dart'; + +class MockWindow extends Mock implements html.Window {} + +void main() { + group('$UrlLauncherPlugin', () { + MockWindow mockWindow = MockWindow(); + UrlLauncherPlugin plugin = UrlLauncherPlugin(window: mockWindow); + + group('canLaunch', () { + test('"http" URLs -> true', () { + expect(plugin.canLaunch('http://google.com'), completion(isTrue)); + }); + + test('"https" URLs -> true', () { + expect(plugin.canLaunch('https://google.com'), completion(isTrue)); + }); + + test('"mailto" URLs -> true', () { + expect( + plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); + }); + + test('"tel" URLs -> false', () { + expect(plugin.canLaunch('tel:5551234567'), completion(isFalse)); + }); + }); + + group('launch', () { + setUp(() { + // Simulate that window.open does something. + when(mockWindow.open('https://www.google.com', '')) + .thenReturn(MockWindow()); + when(mockWindow.open('mailto:name@mydomain.com', '')) + .thenReturn(MockWindow()); + }); + + test('launching a URL returns true', () { + expect( + plugin.launch( + 'https://www.google.com', + useSafariVC: null, + useWebView: null, + universalLinksOnly: null, + enableDomStorage: null, + enableJavaScript: null, + headers: null, + ), + completion(isTrue)); + }); + + test('launching a "mailto" returns true', () { + expect( + plugin.launch( + 'mailto:name@mydomain.com', + useSafariVC: null, + useWebView: null, + universalLinksOnly: null, + enableDomStorage: null, + enableJavaScript: null, + headers: null, + ), + completion(isTrue)); + }); + }); + + group('openNewWindow', () { + bool _standalone; + + setUp(() { + _standalone = navigator.standalone; + }); + + tearDown(() { + navigator.standalone = _standalone; + }); + + test('the window that is launched is a new window', () { + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '')); + }); + + test('the window that is launched is in the same window', () { + navigator.standalone = true; + + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '_top')); + }); + }); + }); +} From 74fd5d1c20708a2a8084c673a19d463dd0d43f26 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 14:17:04 -0700 Subject: [PATCH 2/9] [image_picker_for_web] Remove copypaste from test file. --- .../test/image_picker_for_web_test.dart | 15 +++ .../test/url_launcher_web_test.dart | 103 ------------------ 2 files changed, 15 insertions(+), 103 deletions(-) create mode 100644 packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart delete mode 100644 packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart new file mode 100644 index 000000000000..678dba4a5a30 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class MockWindow extends Mock implements html.Window {} + +void main() { + +} diff --git a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart b/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart deleted file mode 100644 index b6cf8b70460e..000000000000 --- a/packages/image_picker/image_picker_for_web/test/url_launcher_web_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('chrome') // Uses web-only Flutter SDK - -import 'dart:html' as html; -import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; -import 'package:url_launcher_web/src/navigator.dart' as navigator; -import 'package:mockito/mockito.dart'; - -class MockWindow extends Mock implements html.Window {} - -void main() { - group('$UrlLauncherPlugin', () { - MockWindow mockWindow = MockWindow(); - UrlLauncherPlugin plugin = UrlLauncherPlugin(window: mockWindow); - - group('canLaunch', () { - test('"http" URLs -> true', () { - expect(plugin.canLaunch('http://google.com'), completion(isTrue)); - }); - - test('"https" URLs -> true', () { - expect(plugin.canLaunch('https://google.com'), completion(isTrue)); - }); - - test('"mailto" URLs -> true', () { - expect( - plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); - }); - - test('"tel" URLs -> false', () { - expect(plugin.canLaunch('tel:5551234567'), completion(isFalse)); - }); - }); - - group('launch', () { - setUp(() { - // Simulate that window.open does something. - when(mockWindow.open('https://www.google.com', '')) - .thenReturn(MockWindow()); - when(mockWindow.open('mailto:name@mydomain.com', '')) - .thenReturn(MockWindow()); - }); - - test('launching a URL returns true', () { - expect( - plugin.launch( - 'https://www.google.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - test('launching a "mailto" returns true', () { - expect( - plugin.launch( - 'mailto:name@mydomain.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - }); - - group('openNewWindow', () { - bool _standalone; - - setUp(() { - _standalone = navigator.standalone; - }); - - tearDown(() { - navigator.standalone = _standalone; - }); - - test('the window that is launched is a new window', () { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - test('the window that is launched is in the same window', () { - navigator.standalone = true; - - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '_top')); - }); - }); - }); -} From 34e33ecf63404999872ec3dd9c018ac0fe9488ee Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 18 May 2020 17:10:49 -0700 Subject: [PATCH 3/9] Add pick file unit test --- .../lib/image_picker_for_web.dart | 23 ++++--- .../test/image_picker_for_web_test.dart | 64 ++++++++++++++++++- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index cfaf814c65c1..68664ae7b6d4 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -13,14 +13,15 @@ final String _kAcceptVideoMimeType = 'video/*'; /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { + final Function _overrideCreateInput; + bool get _shouldOverrideInput => _overrideCreateInput != null; + html.Element _target; - /// A constructor that allows tests to override the window object used by the plugin. - ImagePickerPlugin({@visibleForTesting html.Element target}) - : _target = target { - if (_target == null) { - _target = _initTarget(_kImagePickerInputsDomId); - } + /// A constructor that allows tests to override the function that creates file inputs. + ImagePickerPlugin({@visibleForTesting Function overrideCreateInput}) + : _overrideCreateInput = overrideCreateInput { + _target = _initTarget(_kImagePickerInputsDomId); } /// Registers this class as the default instance of [ImagePickerPlatform]. @@ -122,6 +123,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { html.Element _createInputElement(String accept, String capture) { html.Element element; + if (_shouldOverrideInput) { + return _overrideCreateInput(accept, capture); + } + if (capture != null) { // Capture is not supported by dart:html :/ element = html.Element.html( @@ -137,8 +142,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Injects the file input element, and clicks on it void _injectAndActivate(html.Element element) { - _target.children.clear(); - _target.children.add(element); + if (!_shouldOverrideInput) { + _target.children.clear(); + _target.children.add(element); + } element.click(); } } diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 678dba4a5a30..0bc6adfe775c 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -4,12 +4,74 @@ @TestOn('chrome') // Uses web-only Flutter SDK +import 'dart:async'; +import 'dart:convert'; import 'dart:html' as html; +import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_for_web/image_picker_for_web.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:mockito/mockito.dart'; -class MockWindow extends Mock implements html.Window {} +final String expectedStringContents = "Hello, world!"; +final Uint8List bytes = utf8.encode(expectedStringContents); +final html.File textFile = html.File([bytes], "hello.txt"); + +class MockFileInput extends Mock implements html.FileUploadInputElement {} + +class MockOnChangeEvent extends Mock implements html.Event { + @override + MockFileInput target; +} + +class MockElementStream extends Mock + implements html.ElementStream { + final StreamController controller = StreamController(); + @override + StreamSubscription listen(void onData(T event), + {Function onError, void onDone(), bool cancelOnError}) { + return controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } +} void main() { + MockFileInput mockInput = MockFileInput(); + MockElementStream mockStream = MockElementStream(); + MockElementStream mockErrorStream = MockElementStream(); + MockOnChangeEvent mockEvent = MockOnChangeEvent()..target = mockInput; + + // Under test... + ImagePickerPlugin plugin = + ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + + setUp(() { + // Make the mockInput behave like a proper input... + when(mockInput.onChange).thenAnswer((_) => mockStream); + when(mockInput.onError).thenAnswer((_) => mockErrorStream); + }); + + tearDown(() { + reset(mockInput); + }); + + // Pick a file... + test('Can select a file, happy case', () async { + // Init the pick file dialog... + final file = plugin.pickImage( + source: ImageSource.gallery, + ); + + // Mock the browser behavior of selecting a file... + when(mockInput.files).thenReturn([textFile]); + mockStream.controller.add(mockEvent); + + // Now the file should be selected + expect(file, completes); + // And readable + expect((await file).readAsString(), completion(expectedStringContents)); + }); + // Creates the correct DOM for the input... } From fd0a6e001d0b65e5e3288bba37c3a9fae85d0d4a Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 18:00:46 -0700 Subject: [PATCH 4/9] [image_picker_for_web] Add tests for web plugin. --- .../lib/image_picker_for_web.dart | 22 ++++-- .../test/image_picker_for_web_test.dart | 74 ++++++++++++++----- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 68664ae7b6d4..940866fcbc71 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -7,6 +7,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; final String _kAcceptImageMimeType = 'image/*'; +// This may not be enough for Safari. final String _kAcceptVideoMimeType = 'video/*'; /// The web implementation of [ImagePickerPlatform]. @@ -37,8 +38,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { int imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String capture = _computeCaptureAttribute(source, preferredCameraDevice); - return _pickFile(accept: _kAcceptImageMimeType, capture: capture); + String capture = computeCaptureAttribute(source, preferredCameraDevice); + return pickFile(accept: _kAcceptImageMimeType, capture: capture); } @override @@ -47,8 +48,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration maxDuration, }) { - String capture = _computeCaptureAttribute(source, preferredCameraDevice); - return _pickFile(accept: _kAcceptVideoMimeType, capture: capture); + String capture = computeCaptureAttribute(source, preferredCameraDevice); + return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } /// Injects a file input with the specified accept+capture attributes, and @@ -56,11 +57,12 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// /// `capture` is only supported in mobile browsers. /// See https://caniuse.com/#feat=html-media-capture - Future _pickFile({ + @visibleForTesting + Future pickFile({ String accept, String capture, }) { - html.FileUploadInputElement input = _createInputElement(accept, capture); + html.FileUploadInputElement input = createInputElement(accept, capture); _injectAndActivate(input); return _getSelectedFile(input); } @@ -68,7 +70,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { // DOM methods /// Converts plugin configuration into a proper value for the `capture` attribute. - String _computeCaptureAttribute(ImageSource source, CameraDevice device) { + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture + @visibleForTesting + String computeCaptureAttribute(ImageSource source, CameraDevice device) { String capture; if (source == ImageSource.camera) { capture = device == CameraDevice.front ? 'user' : 'environment'; @@ -120,7 +125,8 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) - html.Element _createInputElement(String accept, String capture) { + @visibleForTesting + html.Element createInputElement(String accept, String capture) { html.Element element; if (_shouldOverrideInput) { diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 0bc6adfe775c..eaff00a7d40e 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('chrome') // Uses web-only Flutter SDK +@TestOn('chrome') // Uses dart:html import 'dart:async'; import 'dart:convert'; @@ -37,41 +37,79 @@ class MockElementStream extends Mock } void main() { - MockFileInput mockInput = MockFileInput(); - MockElementStream mockStream = MockElementStream(); - MockElementStream mockErrorStream = MockElementStream(); - MockOnChangeEvent mockEvent = MockOnChangeEvent()..target = mockInput; + // Mock the "pick file" browser behavior. + MockFileInput mockInput; + MockElementStream mockStream; + MockElementStream mockErrorStream; + MockOnChangeEvent mockEvent; // Under test... - ImagePickerPlugin plugin = - ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + ImagePickerPlugin plugin; setUp(() { + mockInput = MockFileInput(); + mockStream = MockElementStream(); + mockErrorStream = MockElementStream(); + mockEvent = MockOnChangeEvent()..target = mockInput; + // Make the mockInput behave like a proper input... when(mockInput.onChange).thenAnswer((_) => mockStream); when(mockInput.onError).thenAnswer((_) => mockErrorStream); - }); - tearDown(() { - reset(mockInput); + plugin = ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); }); - // Pick a file... - test('Can select a file, happy case', () async { + test('Can select a file', () async { // Init the pick file dialog... - final file = plugin.pickImage( - source: ImageSource.gallery, - ); + final file = plugin.pickFile(); // Mock the browser behavior of selecting a file... when(mockInput.files).thenReturn([textFile]); mockStream.controller.add(mockEvent); - // Now the file should be selected + // Now the file should be available expect(file, completes); // And readable - expect((await file).readAsString(), completion(expectedStringContents)); + expect((await file).readAsBytes(), completion(isNotEmpty)); + }); + + // There's no good way of detecting when the user has "aborted" the selection. + + test('computeCaptureAttribute', () { + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.rear), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.front), + 'user', + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.rear), + 'environment', + ); }); - // Creates the correct DOM for the input... + group('createInputElement', () { + setUp(() { + plugin = ImagePickerPlugin(); + }); + test('accept: any, capture: null', () { + html.Element input = plugin.createInputElement('any', null); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, isNot(contains('capture'))); + }); + + test('accept: any, capture: something', () { + html.Element input = plugin.createInputElement('any', 'something'); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, containsPair('capture', 'something')); + }); + }); } From 4683ce1848da23af2da3f0ba9aafcfd008443e1b Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 20 May 2020 18:33:10 -0700 Subject: [PATCH 5/9] [image_picker_for_web] Update README.md --- .../image_picker_for_web/README.md | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 3582b604f637..e94ef805cace 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -2,6 +2,42 @@ The web implementation of [`image_picker`][1]. +## Browser Support + +Since Web Browsers don't offer direct access to their users' file system, the web version of the +plugin attempts to approximate those APIs as much as possible. + +### URL.createObjectURL() + +The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL), +which is reasonably well supported across all browsers: + +![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a +local path in your users' drive. See **Use the plugin** below for some examples on how to use this +return value in a cross-platform way. + +### input file "accept" + +In order to filter only video/image content, some browsers offer an [`accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) in their `input type="file"` form elements: + +![Data on support for the input-file-accept feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/input-file-accept.png) + +This feature is just a convenience for users, **not validation**. + +Users can override this setting on their browsers. You must validate in your app (or server) +that the user has picked the file type that you can handle. + +### input file "capture" + +In order to "take a photo", some mobile browsers offer a [`capture` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture): + +![Data on support for the html-media-capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/html-media-capture.png) + +Each browser may implement `capture` any way they please, so it may (or may not) make a +difference in your users' experience. + ## Usage ### Import the package @@ -20,6 +56,32 @@ dependencies: ``` ### Use the plugin -You should be able to use `package:image_picker` as normal. + +You should be able to use `package:image_picker` _almost_ as normal. + +Once the user has picked a file, the returned `PickedFile` instance will contain a +`network`-accessible URL (pointing to a location within the browser). + +The instace will also let you retrieve the bytes of the selected file across all platforms. + +If you want to use the path directly, your code would need look like this: + +```dart +... +if (kIsWeb) { + Image.network(pickedFile.path); +} else { + Image.file(File(pickedFile.path)); +} +... +``` + +Or, using bytes: + +```dart +... +Image.memory(await pickedFile.readAsBytes()) +... +``` [1]: ../image_picker/image_picker From 0e2b9badb2b3c3b1afc4b28da33ad01dfa648274 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 28 May 2020 14:59:03 -0700 Subject: [PATCH 6/9] Depend on the published version of the platform interface. --- packages/image_picker/image_picker_for_web/pubspec.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 1ec71443bc21..094385b99d47 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -14,8 +14,7 @@ flutter: fileName: image_picker_for_web.dart dependencies: - image_picker_platform_interface: - path: ../image_picker_platform_interface + image_picker_platform_interface: ^1.1.0 flutter: sdk: flutter flutter_web_plugins: From 2822d2113d069c83b8a069f3ee64c9ccd4f018e0 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 28 May 2020 17:19:35 -0700 Subject: [PATCH 7/9] Don't lie in the readme. This is not endorsed. --- packages/image_picker/image_picker_for_web/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index e94ef805cace..6e24848e7297 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -1,6 +1,6 @@ # image_picker_for_web -The web implementation of [`image_picker`][1]. +A web implementation of [`image_picker`][1]. ## Browser Support @@ -42,15 +42,16 @@ difference in your users' experience. ### Import the package -This package is the endorsed implementation of `image_picker` for the web platform since version `0.6.7`, so it gets automatically added to your dependencies by depending on `image_picker: ^0.6.7`. +This package is an unendorsed web platform implementation of `image_picker`. -No modifications to your pubspec.yaml should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): +In order to use this, you'll need to depend in `image_picker: ^0.6.7` (which was the first version of the plugin that allowed federation), and `image_picker_for_web: ^0.1.0`. ```yaml ... dependencies: ... image_picker: ^0.6.7 + image_picker_for_web: ^0.1.0 ... ... ``` @@ -84,4 +85,4 @@ Image.memory(await pickedFile.readAsBytes()) ... ``` -[1]: ../image_picker/image_picker +[1]: https://pub.dev/packages/image_picker From d4721207f67c4c39a5cafef92b96aa2a8535ba1e Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Fri, 29 May 2020 15:41:29 -0700 Subject: [PATCH 8/9] Remove mockito and refactor code and tests to do so. --- .../lib/image_picker_for_web.dart | 51 ++++++++++++++----- .../image_picker_for_web/pubspec.yaml | 1 - .../test/image_picker_for_web_test.dart | 51 ++++--------------- 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index 940866fcbc71..f1f9448dc228 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -14,14 +14,15 @@ final String _kAcceptVideoMimeType = 'video/*'; /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { - final Function _overrideCreateInput; - bool get _shouldOverrideInput => _overrideCreateInput != null; + final ImagePickerPluginTestOverrides _overrides; + bool get _hasOverrides => _overrides != null; html.Element _target; /// A constructor that allows tests to override the function that creates file inputs. - ImagePickerPlugin({@visibleForTesting Function overrideCreateInput}) - : _overrideCreateInput = overrideCreateInput { + ImagePickerPlugin({ + @visibleForTesting ImagePickerPluginTestOverrides overrides, + }) : _overrides = overrides { _target = _initTarget(_kImagePickerInputsDomId); } @@ -81,12 +82,19 @@ class ImagePickerPlugin extends ImagePickerPlatform { return capture; } + html.File _getFileFromInput(html.FileUploadInputElement input) { + if (_hasOverrides) { + return _overrides.getFileFromInput(input); + } + return input.files[0]; + } + /// Handles the OnChange event from a FileUploadInputElement object /// Returns the objectURL of the selected file. String _handleOnChangeEvent(html.Event event) { // load the file... final html.FileUploadInputElement input = event.target; - final html.File file = input.files[0]; + final html.File file = _getFileFromInput(input); if (file != null) { return html.Url.createObjectUrl(file); @@ -127,12 +135,12 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// allows to `capture` from the device's cameras (where supported) @visibleForTesting html.Element createInputElement(String accept, String capture) { - html.Element element; - - if (_shouldOverrideInput) { - return _overrideCreateInput(accept, capture); + if (_hasOverrides) { + return _overrides.createInputElement(accept, capture); } + html.Element element; + if (capture != null) { // Capture is not supported by dart:html :/ element = html.Element.html( @@ -148,10 +156,27 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Injects the file input element, and clicks on it void _injectAndActivate(html.Element element) { - if (!_shouldOverrideInput) { - _target.children.clear(); - _target.children.add(element); - } + _target.children.clear(); + _target.children.add(element); element.click(); } } + +// Some tools to override behavior for unit-testing +typedef _OverrideCreateInputFunction = html.Element Function( + String accept, + String capture, +); +typedef _OverrideExtractFilesFromInputFunction = html.File Function( + html.Element, +); + +/// Overrides for some of the functionality above. +@visibleForTesting +class ImagePickerPluginTestOverrides { + /// Override the creation of the input element. + _OverrideCreateInputFunction createInputElement; + + /// Override the extraction of the selected file from an input element. + _OverrideExtractFilesFromInputFunction getFileFromInput; +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 094385b99d47..d25da73847e8 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -26,7 +26,6 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.8.0 - mockito: ^4.1.1 environment: sdk: ">=2.5.0 <3.0.0" diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index eaff00a7d40e..96d048dd2a8e 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -4,7 +4,6 @@ @TestOn('chrome') // Uses dart:html -import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; import 'dart:typed_data'; @@ -12,60 +11,33 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'package:mockito/mockito.dart'; final String expectedStringContents = "Hello, world!"; final Uint8List bytes = utf8.encode(expectedStringContents); final html.File textFile = html.File([bytes], "hello.txt"); -class MockFileInput extends Mock implements html.FileUploadInputElement {} - -class MockOnChangeEvent extends Mock implements html.Event { - @override - MockFileInput target; -} - -class MockElementStream extends Mock - implements html.ElementStream { - final StreamController controller = StreamController(); - @override - StreamSubscription listen(void onData(T event), - {Function onError, void onDone(), bool cancelOnError}) { - return controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } -} - void main() { - // Mock the "pick file" browser behavior. - MockFileInput mockInput; - MockElementStream mockStream; - MockElementStream mockErrorStream; - MockOnChangeEvent mockEvent; - // Under test... ImagePickerPlugin plugin; setUp(() { - mockInput = MockFileInput(); - mockStream = MockElementStream(); - mockErrorStream = MockElementStream(); - mockEvent = MockOnChangeEvent()..target = mockInput; - - // Make the mockInput behave like a proper input... - when(mockInput.onChange).thenAnswer((_) => mockStream); - when(mockInput.onError).thenAnswer((_) => mockErrorStream); - - plugin = ImagePickerPlugin(overrideCreateInput: (_, __) => mockInput); + plugin = ImagePickerPlugin(); }); test('Can select a file', () async { + final mockInput = html.FileUploadInputElement(); + + final overrides = ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getFileFromInput = ((_) => textFile); + + final plugin = ImagePickerPlugin(overrides: overrides); + // Init the pick file dialog... final file = plugin.pickFile(); // Mock the browser behavior of selecting a file... - when(mockInput.files).thenReturn([textFile]); - mockStream.controller.add(mockEvent); + mockInput.dispatchEvent(html.Event('change')); // Now the file should be available expect(file, completes); @@ -95,9 +67,6 @@ void main() { }); group('createInputElement', () { - setUp(() { - plugin = ImagePickerPlugin(); - }); test('accept: any, capture: null', () { html.Element input = plugin.createInputElement('any', null); From 1899117c6a753636b5c6d4bfbf7ab6d6edc049fc Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 2 Jun 2020 15:25:59 -0700 Subject: [PATCH 9/9] Address PR feedback. --- .../image_picker_for_web/README.md | 8 ++- .../lib/image_picker_for_web.dart | 62 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 6e24848e7297..81452e290984 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -4,8 +4,12 @@ A web implementation of [`image_picker`][1]. ## Browser Support -Since Web Browsers don't offer direct access to their users' file system, the web version of the -plugin attempts to approximate those APIs as much as possible. +Since Web Browsers don't offer direct access to their users' file system, +this plugin provides a `PickedFile` abstraction to make access access uniform +across platforms. + +The web version of the plugin puts network-accessible URIs as the `path` +in the returned `PickedFile`. ### URL.createObjectURL() diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index f1f9448dc228..ce99dd6d5fc6 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -7,7 +7,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; final String _kAcceptImageMimeType = 'image/*'; -// This may not be enough for Safari. +// TODO The value below seems to not be enough for Safari (https://github.com/flutter/flutter/issues/58532) final String _kAcceptVideoMimeType = 'video/*'; /// The web implementation of [ImagePickerPlatform]. @@ -23,7 +23,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { ImagePickerPlugin({ @visibleForTesting ImagePickerPluginTestOverrides overrides, }) : _overrides = overrides { - _target = _initTarget(_kImagePickerInputsDomId); + _target = _ensureInitialized(_kImagePickerInputsDomId); } /// Registers this class as the default instance of [ImagePickerPlatform]. @@ -75,25 +75,23 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture @visibleForTesting String computeCaptureAttribute(ImageSource source, CameraDevice device) { - String capture; if (source == ImageSource.camera) { - capture = device == CameraDevice.front ? 'user' : 'environment'; + return (device == CameraDevice.front) ? 'user' : 'environment'; } - return capture; + return null; } html.File _getFileFromInput(html.FileUploadInputElement input) { if (_hasOverrides) { return _overrides.getFileFromInput(input); } - return input.files[0]; + return input?.files?.first; } /// Handles the OnChange event from a FileUploadInputElement object /// Returns the objectURL of the selected file. String _handleOnChangeEvent(html.Event event) { - // load the file... - final html.FileUploadInputElement input = event.target; + final html.FileUploadInputElement input = event?.target; final html.File file = _getFileFromInput(input); if (file != null) { @@ -103,23 +101,28 @@ class ImagePickerPlugin extends ImagePickerPlatform { } /// Monitors an and returns the selected file. - Future _getSelectedFile(html.FileUploadInputElement input) async { - // Observe the input until we can return something + Future _getSelectedFile(html.FileUploadInputElement input) { final Completer _completer = Completer(); - input.onChange.listen((html.Event event) async { + // Observe the input until we can return something + input.onChange.first.then((event) { final objectUrl = _handleOnChangeEvent(event); - _completer.complete(PickedFile(objectUrl)); + if (!_completer.isCompleted) { + _completer.complete(PickedFile(objectUrl)); + } }); - input.onError // What other events signal failure? - .listen((html.Event event) { - _completer.completeError(event); + input.onError.first.then((event) { + if (!_completer.isCompleted) { + _completer.completeError(event); + } }); - + // Note that we don't bother detaching from these streams, since the + // "input" gets re-created in the DOM every time the user needs to + // pick a file. return _completer.future; } /// Initializes a DOM container where we can host input elements. - html.Element _initTarget(String id) { + html.Element _ensureInitialized(String id) { var target = html.querySelector('#${id}'); if (target == null) { final html.Element targetElement = @@ -139,16 +142,10 @@ class ImagePickerPlugin extends ImagePickerPlatform { return _overrides.createInputElement(accept, capture); } - html.Element element; + html.Element element = html.FileUploadInputElement()..accept = accept; if (capture != null) { - // Capture is not supported by dart:html :/ - element = html.Element.html( - '', - validator: html.NodeValidatorBuilder() - ..allowElement('input', attributes: ['type', 'accept', 'capture'])); - } else { - element = html.FileUploadInputElement()..accept = accept; + element.setAttribute('capture', capture); } return element; @@ -163,20 +160,25 @@ class ImagePickerPlugin extends ImagePickerPlatform { } // Some tools to override behavior for unit-testing -typedef _OverrideCreateInputFunction = html.Element Function( +/// A function that creates a file input with the passed in `accept` and `capture` attributes. +@visibleForTesting +typedef OverrideCreateInputFunction = html.Element Function( String accept, String capture, ); -typedef _OverrideExtractFilesFromInputFunction = html.File Function( - html.Element, + +/// A function that extracts a [html.File] from the file `input` passed in. +@visibleForTesting +typedef OverrideExtractFilesFromInputFunction = html.File Function( + html.Element input, ); /// Overrides for some of the functionality above. @visibleForTesting class ImagePickerPluginTestOverrides { /// Override the creation of the input element. - _OverrideCreateInputFunction createInputElement; + OverrideCreateInputFunction createInputElement; /// Override the extraction of the selected file from an input element. - _OverrideExtractFilesFromInputFunction getFileFromInput; + OverrideExtractFilesFromInputFunction getFileFromInput; }