Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[file_selector_web]: Add initial implementation #3141

Merged
merged 19 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/file_selector/file_selector_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 0.7.0

- Initial open-source release.
25 changes: 25 additions & 0 deletions packages/file_selector/file_selector_web/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Copyright 2020 The Flutter 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.
30 changes: 30 additions & 0 deletions packages/file_selector/file_selector_web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# file_picker_web

The web implementation of [`file_picker`][1].

## Usage

### Import the package
To use this plugin in your Flutter Web app, simply add it as a dependency in
your pubspec alongside the base `file_picker` plugin.

_(This is only temporary: in the future we hope to make this package an
"endorsed" implementation of `file_picker`, so that it is automatically
included in your Flutter Web app when you depend on `package:file_picker`.)_

This is what the above means to your `pubspec.yaml`:

```yaml
...
dependencies:
...
file_picker: ^0.7.0
file_picker_web: ^0.7.0
...
```

### Use the plugin
Once you have the `file_picker_web` dependency in your pubspec, you should
be able to use `package:file_picker` as normal.

[1]: ../file_picker/file_picker
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2020 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.

// @dart = 2.9

import 'dart:html';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:file_selector_web/src/dom_helper.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';

void main() {
group('FileSelectorWeb', () {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
DomHelper domHelper;
FileUploadInputElement input;

FileList FileListItems(List<File> files) {
final dataTransfer = DataTransfer();
files.forEach(dataTransfer.items.add);
return dataTransfer.files;
}

void setFilesAndTriggerChange(List<File> files) {
input.files = FileListItems(files);
input.dispatchEvent(Event('change'));
}

setUp(() {
domHelper = DomHelper();
input = FileUploadInputElement();
});

group('getFiles', () {
final mockFile1 = File(['123456'], 'file1.txt');
final mockFile2 = File([], 'file2.txt');

testWidgets('works', (_) async {
final Future<List<XFile>> futureFiles = domHelper.getFiles(
input: input,
);

setFilesAndTriggerChange([mockFile1, mockFile2]);

final List<XFile> files = await futureFiles;

expect(files.length, 2);

expect(files[0].name, 'file1.txt');
expect(await files[0].length(), 6);
expect(await files[0].readAsString(), '123456');
expect(await files[0].lastModified(), isNotNull);

expect(files[1].name, 'file2.txt');
expect(await files[1].length(), 0);
expect(await files[1].readAsString(), '');
expect(await files[1].lastModified(), isNotNull);
});

testWidgets('works multiple times', (_) async {
Future<List<XFile>> futureFiles;
List<XFile> files;

// It should work the first time
futureFiles = domHelper.getFiles(input: input);
setFilesAndTriggerChange([mockFile1]);

files = await futureFiles;

expect(files.length, 1);
expect(files.first.name, mockFile1.name);

// The same input should work more than once
futureFiles = domHelper.getFiles(input: input);
setFilesAndTriggerChange([mockFile2]);

files = await futureFiles;

expect(files.length, 1);
expect(files.first.name, mockFile2.name);
});

testWidgets('sets the <input /> attributes and clicks it', (_) async {
final accept = '.jpg,.png';
final multiple = true;
bool wasClicked = false;

//ignore: unawaited_futures
input.onClick.first.then((_) => wasClicked = true);

final futureFile = domHelper.getFiles(
accept: accept,
multiple: multiple,
input: input,
);

expect(input.matchesWithAncestors('body'), true);
expect(input.accept, accept);
expect(input.multiple, multiple);
expect(
wasClicked,
true,
reason:
'The <input /> should be clicked otherwise no dialog will be shown',
);

setFilesAndTriggerChange([]);
await futureFile;

// It should be already removed from the DOM after the file is resolved.
expect(input.parent, isNull);
});
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2020 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.

// @dart = 2.9

import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:integration_test/integration_test.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
import 'package:file_selector_web/file_selector_web.dart';
import 'package:file_selector_web/src/dom_helper.dart';

void main() {
group('FileSelectorWeb', () {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
MockDomHelper mockDomHelper;
FileSelectorWeb plugin;

setUp(() {
mockDomHelper = MockDomHelper();
plugin = FileSelectorWeb(domHelper: mockDomHelper);
});

group('openFile', () {
final mockFile = createXFile('1001', 'identity.png');

testWidgets('works', (WidgetTester _) async {
final typeGroup = XTypeGroup(
label: 'images',
extensions: ['jpg', 'jpeg'],
mimeTypes: ['image/png'],
webWildCards: ['image/*'],
);

when(mockDomHelper.getFiles(
accept: '.jpg,.jpeg,image/png,image/*',
multiple: false,
)).thenAnswer((_) async => [mockFile]);

final file = await plugin.openFile(acceptedTypeGroups: [typeGroup]);

expect(file.name, mockFile.name);
expect(await file.length(), 4);
expect(await file.readAsString(), '1001');
expect(await file.lastModified(), isNotNull);
});
});

group('openFiles', () {
final mockFile1 = createXFile('123456', 'file1.txt');
final mockFile2 = createXFile('', 'file2.txt');

testWidgets('works', (WidgetTester _) async {
final typeGroup = XTypeGroup(
label: 'files',
extensions: ['.txt'],
);

when(mockDomHelper.getFiles(
accept: '.txt',
multiple: true,
)).thenAnswer((_) async => [mockFile1, mockFile2]);

final files = await plugin.openFiles(acceptedTypeGroups: [typeGroup]);

expect(files.length, 2);

expect(files[0].name, mockFile1.name);
expect(await files[0].length(), 6);
expect(await files[0].readAsString(), '123456');
expect(await files[0].lastModified(), isNotNull);

expect(files[1].name, mockFile2.name);
expect(await files[1].length(), 0);
expect(await files[1].readAsString(), '');
expect(await files[1].lastModified(), isNotNull);
});
});
});
}

class MockDomHelper extends Mock implements DomHelper {}

XFile createXFile(String content, String name) {
final data = Uint8List.fromList(content.codeUnits);
return XFile.fromData(data, name: name, lastModified: DateTime.now());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2020 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 'dart:async';
import 'package:meta/meta.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
import 'package:file_selector_web/src/dom_helper.dart';
import 'package:file_selector_web/src/utils.dart';

/// The web implementation of [FileSelectorPlatform].
///
/// This class implements the `package:file_selector` functionality for the web.
class FileSelectorWeb extends FileSelectorPlatform {
final _domHelper;

/// Registers this class as the default instance of [FileSelectorPlatform].
static void registerWith(Registrar registrar) {
FileSelectorPlatform.instance = FileSelectorWeb();
}

/// Default constructor, initializes _domHelper that we can use
/// to interact with the DOM.
/// overrides parameter allows for testing to override functions
FileSelectorWeb({@visibleForTesting DomHelper domHelper})
: _domHelper = domHelper ?? DomHelper();

@override
Future<XFile> openFile({
List<XTypeGroup> acceptedTypeGroups,
String initialDirectory,
String confirmButtonText,
}) async {
final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups);
return files.first;
}

@override
Future<List<XFile>> openFiles({
List<XTypeGroup> acceptedTypeGroups,
String initialDirectory,
String confirmButtonText,
}) async {
return _openFiles(acceptedTypeGroups: acceptedTypeGroups, multiple: true);
}

@override
Future<String> getSavePath({
List<XTypeGroup> acceptedTypeGroups,
String initialDirectory,
String suggestedName,
String confirmButtonText,
}) async =>
null;

@override
Future<String> getDirectoryPath({
String initialDirectory,
String confirmButtonText,
}) async =>
null;

Future<List<XFile>> _openFiles({
List<XTypeGroup> acceptedTypeGroups,
bool multiple = false,
}) async {
final accept = acceptedTypesToString(acceptedTypeGroups);
return _domHelper.getFiles(
accept: accept,
multiple: multiple,
);
}
}
Loading