Skip to content

Commit

Permalink
[image_picker_for_web] migrates to package:web (flutter#5799)
Browse files Browse the repository at this point in the history
Updates the web implementation of `image_picker_for_web` to `package:web`.

### Issues

* Fixes flutter/flutter#139751
  • Loading branch information
balvinderz committed Mar 6, 2024
1 parent b097d99 commit 9b88dbc
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 155 deletions.
5 changes: 3 additions & 2 deletions packages/image_picker/image_picker_for_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 3.0.3

* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1.
* Migrates package and tests to `package:web`.
* Updates minimum supported SDK version to Flutter 3.19/Dart 3.3.

## 3.0.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@
// found in the LICENSE file.

import 'dart:convert';
import 'dart:html' as html;
import 'dart:js_interop';
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:integration_test/integration_test.dart';
import 'package:web/web.dart' as web;

const String expectedStringContents = 'Hello, world!';
const String otherStringContents = 'Hello again, world!';
final Uint8List bytes = const Utf8Encoder().convert(expectedStringContents);
final Uint8List otherBytes = const Utf8Encoder().convert(otherStringContents);
final Map<String, dynamic> options = <String, dynamic>{
'type': 'text/plain',
'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch,
};
final html.File textFile = html.File(<Uint8List>[bytes], 'hello.txt', options);
final html.File secondTextFile =
html.File(<Uint8List>[otherBytes], 'secondFile.txt');
// TODO(dit): When web:0.6.0 lands, move `type` to the [web.FilePropertyBag] constructor.
// See: https://github.com/dart-lang/web/pull/197
final web.FilePropertyBag options = web.FilePropertyBag(
lastModified: DateTime.utc(2017, 12, 13).millisecondsSinceEpoch,
)..type = 'text/plain';

final web.File textFile =
web.File(<JSUint8Array>[bytes.toJS].toJS, 'hello.txt', options);
final web.File secondTextFile =
web.File(<JSUint8Array>[otherBytes.toJS].toJS, 'secondFile.txt');

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Expand All @@ -36,12 +40,12 @@ void main() {
testWidgets('getImageFromSource can select a file', (
WidgetTester _,
) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();

final web.HTMLInputElement mockInput = web.HTMLInputElement()
..type = 'file';
final ImagePickerPluginTestOverrides overrides =
ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
..getMultipleFilesFromInput = ((_) => <web.File>[textFile]);

final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);

Expand All @@ -50,11 +54,12 @@ void main() {
source: ImageSource.camera,
);

expect(html.querySelector('flt-image-picker-inputs')?.children.isEmpty,
isFalse);
expect(
web.document.querySelector('flt-image-picker-inputs')?.children.length,
isNonZero);

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
mockInput.dispatchEvent(web.Event('change'));

// Now the file should be available
expect(image, completes);
Expand All @@ -69,30 +74,32 @@ void main() {
expect(
file.lastModified(),
completion(
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified),
));
expect(html.querySelector('flt-image-picker-inputs')?.children.isEmpty,
isTrue);
expect(
web.document.querySelector('flt-image-picker-inputs')?.children.length,
isZero);
});

testWidgets('getMultiImageWithOptions can select multiple files', (
WidgetTester _,
) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final web.HTMLInputElement mockInput = web.HTMLInputElement()
..type = 'file';

final ImagePickerPluginTestOverrides overrides =
ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput =
((_) => <html.File>[textFile, secondTextFile]);
((_) => <web.File>[textFile, secondTextFile]);

final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);

// Init the pick file dialog...
final Future<List<XFile>> files = plugin.getMultiImageWithOptions();

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
mockInput.dispatchEvent(web.Event('change'));

// Now the file should be available
expect(files, completes);
Expand All @@ -108,13 +115,14 @@ void main() {
});

testWidgets('getMedia can select multiple files', (WidgetTester _) async {
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
final web.HTMLInputElement mockInput = web.HTMLInputElement()
..type = 'file';

final ImagePickerPluginTestOverrides overrides =
ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput =
((_) => <html.File>[textFile, secondTextFile]);
((_) => <web.File>[textFile, secondTextFile]);

final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);

Expand All @@ -123,7 +131,7 @@ void main() {
plugin.getMedia(options: const MediaOptions(allowMultiple: true));

// Mock the browser behavior of selecting a file...
mockInput.dispatchEvent(html.Event('change'));
mockInput.dispatchEvent(web.Event('change'));

// Now the file should be available
expect(files, completes);
Expand All @@ -139,20 +147,20 @@ void main() {
});

group('cancel event', () {
late html.FileUploadInputElement mockInput;
late web.HTMLInputElement mockInput;
late ImagePickerPluginTestOverrides overrides;
late ImagePickerPlugin plugin;

setUp(() {
mockInput = html.FileUploadInputElement();
mockInput = web.HTMLInputElement()..type = 'file';
overrides = ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
..getMultipleFilesFromInput = ((_) => <web.File>[textFile]);
plugin = ImagePickerPlugin(overrides: overrides);
});

void mockCancel() {
mockInput.dispatchEvent(html.Event('cancel'));
mockInput.dispatchEvent(web.Event('cancel'));
}

testWidgets('getFiles - returns empty list', (WidgetTester _) async {
Expand Down Expand Up @@ -226,61 +234,61 @@ void main() {

group('createInputElement', () {
testWidgets('accept: any, capture: null', (WidgetTester tester) async {
final html.Element input = plugin.createInputElement('any', null);
final web.Element input = plugin.createInputElement('any', null);

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, isNot(contains('capture')));
expect(input.attributes, isNot(contains('multiple')));
expect(input.getAttribute('accept'), 'any');
expect(input.hasAttribute('capture'), false);
expect(input.hasAttribute('multiple'), false);
});

testWidgets('accept: any, capture: something', (WidgetTester tester) async {
final html.Element input = plugin.createInputElement('any', 'something');
final web.Element input = plugin.createInputElement('any', 'something');

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, containsPair('capture', 'something'));
expect(input.attributes, isNot(contains('multiple')));
expect(input.getAttribute('accept'), 'any');
expect(input.getAttribute('capture'), 'something');
expect(input.hasAttribute('multiple'), false);
});

testWidgets('accept: any, capture: null, multi: true',
(WidgetTester tester) async {
final html.Element input =
final web.Element input =
plugin.createInputElement('any', null, multiple: true);

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, isNot(contains('capture')));
expect(input.attributes, contains('multiple'));
expect(input.getAttribute('accept'), 'any');
expect(input.hasAttribute('capture'), false);
expect(input.hasAttribute('multiple'), true);
});

testWidgets('accept: any, capture: something, multi: true',
(WidgetTester tester) async {
final html.Element input =
final web.Element input =
plugin.createInputElement('any', 'something', multiple: true);

expect(input.attributes, containsPair('accept', 'any'));
expect(input.attributes, containsPair('capture', 'something'));
expect(input.attributes, contains('multiple'));
expect(input.getAttribute('accept'), 'any');
expect(input.getAttribute('capture'), 'something');
expect(input.hasAttribute('multiple'), true);
});
});

group('Deprecated methods', () {
late html.FileUploadInputElement mockInput;
late web.HTMLInputElement mockInput;
late ImagePickerPluginTestOverrides overrides;
late ImagePickerPlugin plugin;

setUp(() {
mockInput = html.FileUploadInputElement();
mockInput = web.HTMLInputElement()..type = 'file';
overrides = ImagePickerPluginTestOverrides()
..createInputElement = ((_, __) => mockInput)
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
..getMultipleFilesFromInput = ((_) => <web.File>[textFile]);
plugin = ImagePickerPlugin(overrides: overrides);
});

void mockCancel() {
mockInput.dispatchEvent(html.Event('cancel'));
mockInput.dispatchEvent(web.Event('cancel'));
}

void mockChange() {
mockInput.dispatchEvent(html.Event('change'));
mockInput.dispatchEvent(web.Event('change'));
}

group('getImage', () {
Expand All @@ -306,7 +314,7 @@ void main() {
expect(
file.lastModified(),
completion(
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified),
));
});

Expand All @@ -326,7 +334,7 @@ void main() {
testWidgets('can select multiple files', (WidgetTester _) async {
// Override the returned files...
overrides.getMultipleFilesFromInput =
(_) => <html.File>[textFile, secondTextFile];
(_) => <web.File>[textFile, secondTextFile];

// ignore: deprecated_member_use
final Future<List<XFile>> files = plugin.getMultiImage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:html' as html;
import 'dart:js_interop';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter_test/flutter_test.dart';
import 'package:image_picker_for_web/src/image_resizer.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:integration_test/integration_test.dart';
import 'package:web/helpers.dart';
import 'package:web/web.dart' as web;

//This is a sample 10x10 png image
const String pngFileBase64Contents =
Expand All @@ -24,14 +26,13 @@ void main() {
late XFile pngFile;
setUp(() {
imageResizer = ImageResizer();
final html.File pngHtmlFile =
_base64ToFile(pngFileBase64Contents, 'pngImage.png');
pngFile = XFile(html.Url.createObjectUrl(pngHtmlFile),
name: pngHtmlFile.name, mimeType: pngHtmlFile.type);
final web.Blob pngHtmlFile = _base64ToBlob(pngFileBase64Contents);
pngFile = XFile(web.URL.createObjectURL(pngHtmlFile),
name: 'pngImage.png', mimeType: 'image/png');
});

testWidgets('image is loaded correctly ', (WidgetTester tester) async {
final html.ImageElement imageElement =
final web.HTMLImageElement imageElement =
await imageResizer.loadImage(pngFile.path);
expect(imageElement.width, 10);
expect(imageElement.height, 10);
Expand All @@ -40,9 +41,9 @@ void main() {
testWidgets(
"canvas is loaded with image's width and height when max width and max height are null",
(WidgetTester widgetTester) async {
final html.ImageElement imageElement =
final web.HTMLImageElement imageElement =
await imageResizer.loadImage(pngFile.path);
final html.CanvasElement canvas =
final web.HTMLCanvasElement canvas =
imageResizer.resizeImageElement(imageElement, null, null);
expect(canvas.width, imageElement.width);
expect(canvas.height, imageElement.height);
Expand All @@ -51,19 +52,19 @@ void main() {
testWidgets(
'canvas size is scaled when max width and max height are not null',
(WidgetTester widgetTester) async {
final html.ImageElement imageElement =
final web.HTMLImageElement imageElement =
await imageResizer.loadImage(pngFile.path);
final html.CanvasElement canvas =
final web.HTMLCanvasElement canvas =
imageResizer.resizeImageElement(imageElement, 8, 8);
expect(canvas.width, 8);
expect(canvas.height, 8);
});

testWidgets('resized image is returned after converting canvas to file',
(WidgetTester widgetTester) async {
final html.ImageElement imageElement =
final web.HTMLImageElement imageElement =
await imageResizer.loadImage(pngFile.path);
final html.CanvasElement canvas =
final web.HTMLCanvasElement canvas =
imageResizer.resizeImageElement(imageElement, null, null);
final XFile resizedImage =
await imageResizer.writeCanvasToFile(pngFile, canvas, null);
Expand Down Expand Up @@ -112,19 +113,21 @@ void main() {

Future<Size> _getImageSize(XFile file) async {
final Completer<Size> completer = Completer<Size>();
final html.ImageElement image = html.ImageElement(src: file.path);
image.onLoad.listen((html.Event event) {
completer.complete(Size(image.width!.toDouble(), image.height!.toDouble()));
});
image.onError.listen((html.Event event) {
completer.complete(Size.zero);
});
final web.HTMLImageElement image = web.HTMLImageElement();
image
..onLoad.listen((web.Event event) {
completer.complete(Size(image.width.toDouble(), image.height.toDouble()));
})
..onError.listen((web.Event event) {
completer.complete(Size.zero);
})
..src = file.path;
return completer.future;
}

html.File _base64ToFile(String data, String fileName) {
web.Blob _base64ToBlob(String data) {
final List<String> arr = data.split(',');
final String bstr = html.window.atob(arr[1]);
final String bstr = web.window.atob(arr[1]);
int n = bstr.length;
final Uint8List u8arr = Uint8List(n);

Expand All @@ -133,5 +136,5 @@ html.File _base64ToFile(String data, String fileName) {
n--;
}

return html.File(<Uint8List>[u8arr], fileName);
return Blob(<JSUint8Array>[u8arr.toJS].toJS);
}

0 comments on commit 9b88dbc

Please sign in to comment.