[image_picker_for_web] Migrate image_picker to package:cross_file (#4083)

* Migrate image picker platform interface to cross_file
* Port tests from --platform=chrome to integration_test

Co-authored-by: David Iglesias Teixeira <ditman@gmail.com>
diff --git a/.cirrus.yml b/.cirrus.yml
index 4ad5e8e..8f69bd1 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -168,7 +168,7 @@
       env:
         # Currently missing; see https://github.com/flutter/flutter/issues/81982
         # and https://github.com/flutter/flutter/issues/82211
-        PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "file_selector,image_picker_for_web,shared_preferences_web"
+        PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "file_selector,shared_preferences_web"
         matrix:
           CHANNEL: "master"
           CHANNEL: "stable"
diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
index 7b2c407..b0379ad 100644
--- a/packages/image_picker/image_picker_for_web/CHANGELOG.md
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 2.1.0
+
+* Implemented `getImage`, `getVideo` and `getFile` methods that return `XFile` instances.
+* Move tests to `example` directory, so they run as integration_tests with `flutter drive`.
+
 # 2.0.0
 
 * Migrate to null safety.
diff --git a/packages/image_picker/image_picker_for_web/example/README.md b/packages/image_picker/image_picker_for_web/example/README.md
new file mode 100644
index 0000000..4348451
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/README.md
@@ -0,0 +1,9 @@
+# Testing
+
+This package uses `package:integration_test` to run its tests in a web browser.
+
+See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests)
+in the Flutter wiki for instructions to setup and run the tests in this package.
+
+Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests)
+for more info.
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/example/integration_test/image_picker_for_web_test.dart
similarity index 68%
rename from packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart
rename to packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
index fbdd1d3..c6d0b3b 100644
--- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart
+++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-@TestOn('chrome') // Uses dart:html
-
 import 'dart:convert';
 import 'dart:html' as html;
 import 'dart:typed_data';
@@ -11,12 +9,15 @@
 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:integration_test/integration_test.dart';
 
 final String expectedStringContents = "Hello, world!";
 final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List;
 final html.File textFile = html.File([bytes], "hello.txt");
 
 void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
   // Under test...
   late ImagePickerPlugin plugin;
 
@@ -24,7 +25,7 @@
     plugin = ImagePickerPlugin();
   });
 
-  test('Can select a file', () async {
+  testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async {
     final mockInput = html.FileUploadInputElement();
 
     final overrides = ImagePickerPluginTestOverrides()
@@ -45,9 +46,30 @@
     expect((await file).readAsBytes(), completion(isNotEmpty));
   });
 
+  testWidgets('Can select a file', (WidgetTester tester) 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.getFile();
+
+    // Mock the browser behavior of selecting a file...
+    mockInput.dispatchEvent(html.Event('change'));
+
+    // Now the file should be available
+    expect(file, completes);
+    // And readable
+    expect((await file).readAsBytes(), completion(isNotEmpty));
+  });
+
   // There's no good way of detecting when the user has "aborted" the selection.
 
-  test('computeCaptureAttribute', () {
+  testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
     expect(
       plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front),
       isNull,
@@ -67,14 +89,14 @@
   });
 
   group('createInputElement', () {
-    test('accept: any, capture: null', () {
+    testWidgets('accept: any, capture: null', (WidgetTester tester) async {
       html.Element input = plugin.createInputElement('any', null);
 
       expect(input.attributes, containsPair('accept', 'any'));
       expect(input.attributes, isNot(contains('capture')));
     });
 
-    test('accept: any, capture: something', () {
+    testWidgets('accept: any, capture: something', (WidgetTester tester) async {
       html.Element input = plugin.createInputElement('any', 'something');
 
       expect(input.attributes, containsPair('accept', 'any'));
diff --git a/packages/image_picker/image_picker_for_web/example/lib/main.dart b/packages/image_picker/image_picker_for_web/example/lib/main.dart
new file mode 100644
index 0000000..e1a38dc
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/lib/main.dart
@@ -0,0 +1,25 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+void main() {
+  runApp(MyApp());
+}
+
+/// App for testing
+class MyApp extends StatefulWidget {
+  @override
+  _MyAppState createState() => _MyAppState();
+}
+
+class _MyAppState extends State<MyApp> {
+  @override
+  Widget build(BuildContext context) {
+    return Directionality(
+      textDirection: TextDirection.ltr,
+      child: Text('Testing... Look at the console output for results!'),
+    );
+  }
+}
diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml
new file mode 100644
index 0000000..8dadde2
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml
@@ -0,0 +1,21 @@
+name: connectivity_for_web_integration_tests
+publish_to: none
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=2.2.0"
+
+dependencies:
+  image_picker_for_web:
+    path: ../
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  js: ^0.6.3
+  flutter_test:
+    sdk: flutter
+  flutter_driver:
+    sdk: flutter
+  integration_test:
+    sdk: flutter
diff --git a/packages/image_picker/image_picker_for_web/example/run_test.sh b/packages/image_picker/image_picker_for_web/example/run_test.sh
new file mode 100755
index 0000000..aa52974
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/run_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/bash
+# 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.
+
+if pgrep -lf chromedriver > /dev/null; then
+  echo "chromedriver is running."
+
+  if [ $# -eq 0 ]; then
+    echo "No target specified, running all tests..."
+    find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}'
+  else
+    echo "Running test target: $1..."
+    set -x
+    flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1
+  fi
+
+  else
+    echo "chromedriver is not running."
+    echo "Please, check the README.md for instructions on how to use run_test.sh"
+fi
+
diff --git a/packages/image_picker/image_picker_for_web/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_for_web/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..4f10f2a
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:integration_test/integration_test_driver.dart';
+
+Future<void> main() => integrationDriver();
diff --git a/packages/image_picker/image_picker_for_web/example/web/index.html b/packages/image_picker/image_picker_for_web/example/web/index.html
new file mode 100644
index 0000000..7fb138c
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/example/web/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>example</title>
+</head>
+<body>
+  <script src="main.dart.js" type="application/javascript"></script>
+</body>
+</html>
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 2fb6638..08ce801 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
@@ -96,6 +96,68 @@
     return _getSelectedFile(input);
   }
 
+  /// Returns an [XFile] with the image that was picked.
+  ///
+  /// The `source` argument controls where the image comes from. This can
+  /// be either [ImageSource.camera] or [ImageSource.gallery].
+  ///
+  /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin.
+  ///
+  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+  /// Defaults to [CameraDevice.rear].
+  ///
+  /// If no images were picked, the return value is null.
+  @override
+  Future<XFile> getImage({
+    required ImageSource source,
+    double? maxWidth,
+    double? maxHeight,
+    int? imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) {
+    String? capture = computeCaptureAttribute(source, preferredCameraDevice);
+    return getFile(accept: _kAcceptImageMimeType, capture: capture);
+  }
+
+  /// Returns an [XFile] containing the video that was picked.
+  ///
+  /// The [source] argument controls where the video comes from. This can
+  /// be either [ImageSource.camera] or [ImageSource.gallery].
+  ///
+  /// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin.
+  ///
+  /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+  /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+  /// Defaults to [CameraDevice.rear].
+  ///
+  /// If no images were picked, the return value is null.
+  @override
+  Future<XFile> getVideo({
+    required ImageSource source,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    Duration? maxDuration,
+  }) {
+    String? capture = computeCaptureAttribute(source, preferredCameraDevice);
+    return getFile(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
+  @visibleForTesting
+  Future<XFile> getFile({
+    String? accept,
+    String? capture,
+  }) {
+    html.FileUploadInputElement input =
+        createInputElement(accept, capture) as html.FileUploadInputElement;
+    _injectAndActivate(input);
+    return _getSelectedXFile(input);
+  }
+
   // DOM methods
 
   /// Converts plugin configuration into a proper value for the `capture` attribute.
@@ -150,6 +212,26 @@
     return _completer.future;
   }
 
+  Future<XFile> _getSelectedXFile(html.FileUploadInputElement input) {
+    final Completer<XFile> _completer = Completer<XFile>();
+    // Observe the input until we can return something
+    input.onChange.first.then((event) {
+      final objectUrl = _handleOnChangeEvent(event);
+      if (!_completer.isCompleted && objectUrl != null) {
+        _completer.complete(XFile(objectUrl));
+      }
+    });
+    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 _ensureInitialized(String id) {
     var target = html.querySelector('#${id}');
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
index 768f7e2..d9b9c5e 100644
--- a/packages/image_picker/image_picker_for_web/pubspec.yaml
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of image_picker
 repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 2.0.0
+version: 2.1.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -20,7 +20,7 @@
     sdk: flutter
   flutter_web_plugins:
     sdk: flutter
-  image_picker_platform_interface: ^2.0.0
+  image_picker_platform_interface: ^2.2.0
   meta: ^1.3.0
 
 dev_dependencies:
diff --git a/packages/image_picker/image_picker_for_web/test/README.md b/packages/image_picker/image_picker_for_web/test/README.md
new file mode 100644
index 0000000..7c5b4ad
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/test/README.md
@@ -0,0 +1,5 @@
+## test
+
+This package uses integration tests for testing.
+
+See `example/README.md` for more info.
diff --git a/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart b/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart
new file mode 100644
index 0000000..442c501
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart
@@ -0,0 +1,14 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  test('Tell the user where to find the real tests', () {
+    print('---');
+    print('This package uses integration_test for its tests.');
+    print('See `example/README.md` for more info.');
+    print('---');
+  });
+}