Skip to content

Commit ce53da1

Browse files
authored
[image_picker_web] Listens to file input cancel event. (#4453)
## Changes This PR listens to the `cancel` event from the `input type=file` used by the web implementation of the image_picker plugin, so apps don't end up endlessly awaiting for a file that will never come **in modern browsers** (Chrome 113, Safari 16.4, or newer). _Same API as #3683 Additionally, this PR: * Removes all code and tests mentioning `PickedFile`. (Deprecated years ago, and unused since #4285) **(Breaking change)** * Updates README to mention `XFile` which is the current return type of the package. * Updates the dependency on `image_picker_platform_interface` to `^2.9.0`. * Implements all non-deprecated methods from the interface, and makes deprecated methods use the fresh ones. * Updates tests. ### Issues * Fixes flutter/flutter#92176 ### Testing * Added integration testing coverage for the 'cancel' event. * Tested manually in Chrome with the example app running on web.
1 parent bf8e503 commit ce53da1

File tree

5 files changed

+303
-189
lines changed

5 files changed

+303
-189
lines changed

packages/image_picker/image_picker_for_web/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 3.0.0
2+
3+
* **BREAKING CHANGE:** Removes all code and tests mentioning `PickedFile`.
4+
* Listens to `cancel` event on file selection. When the selection is canceled:
5+
* `Future<XFile?>` methods return `null`
6+
* `Future<List<XFile>>` methods return an empty list.
7+
18
## 2.2.0
29

310
* Adds `getMedia` method.

packages/image_picker/image_picker_for_web/README.md

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,12 @@ A web implementation of [`image_picker`][1].
44

55
## Limitations on the web platform
66

7-
Since Web Browsers don't offer direct access to their users' file system,
8-
this plugin provides a `PickedFile` abstraction to make access uniform
9-
across platforms.
7+
### `XFile`
108

11-
The web version of the plugin puts network-accessible URIs as the `path`
12-
in the returned `PickedFile`.
9+
This plugin uses `XFile` objects to abstract files picked/created by the user.
1310

14-
### URL.createObjectURL()
15-
16-
The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL),
17-
which is reasonably well supported across all browsers:
18-
19-
![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png)
20-
21-
However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a
22-
local path in your users' drive. See **Use the plugin** below for some examples on how to use this
23-
return value in a cross-platform way.
11+
Read more about `XFile` on the web in
12+
[`package:cross_file`'s README](https://pub.dev/packages/cross_file).
2413

2514
### input file "accept"
2615

@@ -42,11 +31,26 @@ In order to "take a photo", some mobile browsers offer a [`capture` attribute](h
4231
Each browser may implement `capture` any way they please, so it may (or may not) make a
4332
difference in your users' experience.
4433

45-
### pickImage()
46-
The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported for gif images.
47-
The argument `imageQuality` only works for jpeg and webp images.
34+
### input file "cancel"
35+
36+
The [`cancel` event](https://caniuse.com/mdn-api_htmlinputelement_cancel_event)
37+
used by the plugin to detect when users close the file selector without picking
38+
a file is relatively new, and will only work in recent browsers.
39+
40+
### `ImagePickerOptions` support
41+
42+
The `ImagePickerOptions` configuration object allows passing resize (`maxWidth`,
43+
`maxHeight`) and quality (`imageQuality`) parameters to some methods of this
44+
plugin, which in other platforms control how selected images are resized or
45+
re-encoded.
46+
47+
On the web:
48+
49+
* `maxWidth`, `maxHeight` and `imageQuality` are not supported for `gif` images.
50+
* `imageQuality` only affects `jpg` and `webp` images.
51+
52+
### `getVideo()`
4853

49-
### pickVideo()
5054
The argument `maxDuration` is not supported on the web.
5155

5256
## Usage
@@ -65,8 +69,8 @@ should add it to your `pubspec.yaml` as usual.
6569

6670
You should be able to use `package:image_picker` _almost_ as normal.
6771

68-
Once the user has picked a file, the returned `PickedFile` instance will contain a
69-
`network`-accessible URL (pointing to a location within the browser).
72+
Once the user has picked a file, the returned `XFile` instance will contain a
73+
`network`-accessible `Blob` URL (pointing to a location within the browser).
7074

7175
The instance will also let you retrieve the bytes of the selected file across all platforms.
7276

packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart

Lines changed: 178 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ void main() {
3333
plugin = ImagePickerPlugin();
3434
});
3535

36-
testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async {
36+
testWidgets('getImageFromSource can select a file', (
37+
WidgetTester _,
38+
) async {
3739
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
3840

3941
final ImagePickerPluginTestOverrides overrides =
@@ -44,29 +46,9 @@ void main() {
4446
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
4547

4648
// Init the pick file dialog...
47-
final Future<PickedFile> file = plugin.pickFile();
48-
49-
// Mock the browser behavior of selecting a file...
50-
mockInput.dispatchEvent(html.Event('change'));
51-
52-
// Now the file should be available
53-
expect(file, completes);
54-
// And readable
55-
expect((await file).readAsBytes(), completion(isNotEmpty));
56-
});
57-
58-
testWidgets('Can select a file', (WidgetTester tester) async {
59-
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
60-
61-
final ImagePickerPluginTestOverrides overrides =
62-
ImagePickerPluginTestOverrides()
63-
..createInputElement = ((_, __) => mockInput)
64-
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
65-
66-
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
67-
68-
// Init the pick file dialog...
69-
final Future<XFile> image = plugin.getImage(source: ImageSource.camera);
49+
final Future<XFile?> image = plugin.getImageFromSource(
50+
source: ImageSource.camera,
51+
);
7052

7153
// Mock the browser behavior of selecting a file...
7254
mockInput.dispatchEvent(html.Event('change'));
@@ -75,8 +57,9 @@ void main() {
7557
expect(image, completes);
7658

7759
// And readable
78-
final XFile file = await image;
79-
expect(file.readAsBytes(), completion(isNotEmpty));
60+
final XFile? file = await image;
61+
expect(file, isNotNull);
62+
expect(file!.readAsBytes(), completion(isNotEmpty));
8063
expect(file.name, textFile.name);
8164
expect(file.length(), completion(textFile.size));
8265
expect(file.mimeType, textFile.type);
@@ -87,8 +70,9 @@ void main() {
8770
));
8871
});
8972

90-
testWidgets('getMultiImage can select multiple files',
91-
(WidgetTester tester) async {
73+
testWidgets('getMultiImageWithOptions can select multiple files', (
74+
WidgetTester _,
75+
) async {
9276
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
9377

9478
final ImagePickerPluginTestOverrides overrides =
@@ -100,7 +84,7 @@ void main() {
10084
final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides);
10185

10286
// Init the pick file dialog...
103-
final Future<List<XFile>> files = plugin.getMultiImage();
87+
final Future<List<XFile>> files = plugin.getMultiImageWithOptions();
10488

10589
// Mock the browser behavior of selecting a file...
10690
mockInput.dispatchEvent(html.Event('change'));
@@ -118,8 +102,7 @@ void main() {
118102
expect(secondFile.length(), completion(secondTextFile.size));
119103
});
120104

121-
testWidgets('getMedia can select multiple files',
122-
(WidgetTester tester) async {
105+
testWidgets('getMedia can select multiple files', (WidgetTester _) async {
123106
final html.FileUploadInputElement mockInput = html.FileUploadInputElement();
124107

125108
final ImagePickerPluginTestOverrides overrides =
@@ -150,7 +133,72 @@ void main() {
150133
expect(secondFile.length(), completion(secondTextFile.size));
151134
});
152135

153-
// There's no good way of detecting when the user has "aborted" the selection.
136+
group('cancel event', () {
137+
late html.FileUploadInputElement mockInput;
138+
late ImagePickerPluginTestOverrides overrides;
139+
late ImagePickerPlugin plugin;
140+
141+
setUp(() {
142+
mockInput = html.FileUploadInputElement();
143+
overrides = ImagePickerPluginTestOverrides()
144+
..createInputElement = ((_, __) => mockInput)
145+
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
146+
plugin = ImagePickerPlugin(overrides: overrides);
147+
});
148+
149+
void mockCancel() {
150+
mockInput.dispatchEvent(html.Event('cancel'));
151+
}
152+
153+
testWidgets('getFiles - returns empty list', (WidgetTester _) async {
154+
final Future<List<XFile>> files = plugin.getFiles();
155+
mockCancel();
156+
157+
expect(files, completes);
158+
expect(await files, isEmpty);
159+
});
160+
161+
testWidgets('getMedia - returns empty list', (WidgetTester _) async {
162+
final Future<List<XFile>?> files = plugin.getMedia(
163+
options: const MediaOptions(
164+
allowMultiple: true,
165+
));
166+
mockCancel();
167+
168+
expect(files, completes);
169+
expect(await files, isEmpty);
170+
});
171+
172+
testWidgets('getMultiImageWithOptions - returns empty list', (
173+
WidgetTester _,
174+
) async {
175+
final Future<List<XFile>?> files = plugin.getMultiImageWithOptions();
176+
mockCancel();
177+
178+
expect(files, completes);
179+
expect(await files, isEmpty);
180+
});
181+
182+
testWidgets('getImageFromSource - returns null', (WidgetTester _) async {
183+
final Future<XFile?> file = plugin.getImageFromSource(
184+
source: ImageSource.gallery,
185+
);
186+
mockCancel();
187+
188+
expect(file, completes);
189+
expect(await file, isNull);
190+
});
191+
192+
testWidgets('getVideo - returns null', (WidgetTester _) async {
193+
final Future<XFile?> file = plugin.getVideo(
194+
source: ImageSource.gallery,
195+
);
196+
mockCancel();
197+
198+
expect(file, completes);
199+
expect(await file, isNull);
200+
});
201+
});
154202

155203
testWidgets('computeCaptureAttribute', (WidgetTester tester) async {
156204
expect(
@@ -208,4 +256,102 @@ void main() {
208256
expect(input.attributes, contains('multiple'));
209257
});
210258
});
259+
260+
group('Deprecated methods', () {
261+
late html.FileUploadInputElement mockInput;
262+
late ImagePickerPluginTestOverrides overrides;
263+
late ImagePickerPlugin plugin;
264+
265+
setUp(() {
266+
mockInput = html.FileUploadInputElement();
267+
overrides = ImagePickerPluginTestOverrides()
268+
..createInputElement = ((_, __) => mockInput)
269+
..getMultipleFilesFromInput = ((_) => <html.File>[textFile]);
270+
plugin = ImagePickerPlugin(overrides: overrides);
271+
});
272+
273+
void mockCancel() {
274+
mockInput.dispatchEvent(html.Event('cancel'));
275+
}
276+
277+
void mockChange() {
278+
mockInput.dispatchEvent(html.Event('change'));
279+
}
280+
281+
group('getImage', () {
282+
testWidgets('can select a file', (WidgetTester _) async {
283+
// ignore: deprecated_member_use
284+
final Future<XFile?> image = plugin.getImage(
285+
source: ImageSource.camera,
286+
);
287+
288+
// Mock the browser behavior when selecting a file...
289+
mockChange();
290+
291+
// Now the file should be available
292+
expect(image, completes);
293+
294+
// And readable
295+
final XFile? file = await image;
296+
expect(file, isNotNull);
297+
expect(file!.readAsBytes(), completion(isNotEmpty));
298+
expect(file.name, textFile.name);
299+
expect(file.length(), completion(textFile.size));
300+
expect(file.mimeType, textFile.type);
301+
expect(
302+
file.lastModified(),
303+
completion(
304+
DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!),
305+
));
306+
});
307+
308+
testWidgets('returns null when canceled', (WidgetTester _) async {
309+
// ignore: deprecated_member_use
310+
final Future<XFile?> file = plugin.getImage(
311+
source: ImageSource.gallery,
312+
);
313+
mockCancel();
314+
315+
expect(file, completes);
316+
expect(await file, isNull);
317+
});
318+
});
319+
320+
group('getMultiImage', () {
321+
testWidgets('can select multiple files', (WidgetTester _) async {
322+
// Override the returned files...
323+
overrides.getMultipleFilesFromInput =
324+
(_) => <html.File>[textFile, secondTextFile];
325+
326+
// ignore: deprecated_member_use
327+
final Future<List<XFile>> files = plugin.getMultiImage();
328+
329+
// Mock the browser behavior of selecting a file...
330+
mockChange();
331+
332+
// Now the file should be available
333+
expect(files, completes);
334+
335+
// And readable
336+
expect((await files).first.readAsBytes(), completion(isNotEmpty));
337+
338+
// Peek into the second file...
339+
final XFile secondFile = (await files).elementAt(1);
340+
expect(secondFile.readAsBytes(), completion(isNotEmpty));
341+
expect(secondFile.name, secondTextFile.name);
342+
expect(secondFile.length(), completion(secondTextFile.size));
343+
});
344+
345+
testWidgets('returns an empty list when canceled', (
346+
WidgetTester _,
347+
) async {
348+
// ignore: deprecated_member_use
349+
final Future<List<XFile>?> files = plugin.getMultiImage();
350+
mockCancel();
351+
352+
expect(files, completes);
353+
expect(await files, isEmpty);
354+
});
355+
});
356+
});
211357
}

0 commit comments

Comments
 (0)