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

Commit 39158c0

Browse files
adpinolaeugerossetto
authored andcommitted
add file_selector_api Dart implementation
add test suite for file_selector_api
1 parent 8d1b217 commit 39158c0

File tree

5 files changed

+449
-0
lines changed

5 files changed

+449
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/cupertino.dart';
6+
import 'package:win32/win32.dart';
7+
8+
import 'file_selector_dart/dialog_mode.dart';
9+
import 'file_selector_dart/dialog_wrapper.dart';
10+
import 'file_selector_dart/dialog_wrapper_factory.dart';
11+
import 'file_selector_dart/selection_options.dart';
12+
13+
/// File dialog handling for Open and Save operations.
14+
class FileSelectorApi {
15+
/// Creates a new instance of [FileSelectorApi].
16+
/// Allows Dependency Injection of a [DialogWrapperFactory] to handle dialog creation.
17+
FileSelectorApi(this._dialogWrapperFactory)
18+
: _foregroundWindow = GetForegroundWindow();
19+
20+
/// Creates a fake instance of [FileSelectorApi] for testing purpose where the [_foregroundWindow] handle is set
21+
/// from the outside.
22+
@visibleForTesting
23+
FileSelectorApi.useFakeForegroundWindow(
24+
this._dialogWrapperFactory, this._foregroundWindow);
25+
26+
final DialogWrapperFactory _dialogWrapperFactory;
27+
28+
final int _foregroundWindow;
29+
30+
/// Displays a dialog window to open one or more files.
31+
List<String?> showOpenDialog(
32+
SelectionOptions options,
33+
String? initialDirectory,
34+
String? confirmButtonText,
35+
) =>
36+
_showDialog(_foregroundWindow, DialogMode.Open, options, initialDirectory,
37+
null, confirmButtonText);
38+
39+
/// Displays a dialog used to save a file.
40+
List<String?> showSaveDialog(
41+
SelectionOptions options,
42+
String? initialDirectory,
43+
String? suggestedName,
44+
String? confirmButtonText,
45+
) =>
46+
_showDialog(_foregroundWindow, DialogMode.Save, options, initialDirectory,
47+
suggestedName, confirmButtonText);
48+
49+
List<String?> _showDialog(
50+
int parentWindow,
51+
DialogMode mode,
52+
SelectionOptions options,
53+
String? initialDirectory,
54+
String? suggestedName,
55+
String? confirmLabel) {
56+
final DialogWrapper dialogWrapper =
57+
_dialogWrapperFactory.createInstance(mode);
58+
if (!SUCCEEDED(dialogWrapper.lastResult)) {
59+
throw WindowsException(E_FAIL);
60+
}
61+
int dialogOptions = 0;
62+
if (options.selectFolders) {
63+
dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS;
64+
}
65+
if (options.allowMultiple) {
66+
dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT;
67+
}
68+
if (dialogOptions != 0) {
69+
dialogWrapper.addOptions(dialogOptions);
70+
}
71+
72+
if (initialDirectory != null) {
73+
dialogWrapper.setFolder(initialDirectory);
74+
}
75+
if (suggestedName != null) {
76+
dialogWrapper.setFileName(suggestedName);
77+
}
78+
if (confirmLabel != null) {
79+
dialogWrapper.setOkButtonLabel(confirmLabel);
80+
}
81+
82+
if (options.allowedTypes.isNotEmpty) {
83+
dialogWrapper.setFileTypeFilters(options.allowedTypes);
84+
}
85+
86+
final List<String?>? files = dialogWrapper.show(parentWindow);
87+
if (files != null) {
88+
return files;
89+
}
90+
throw WindowsException(E_FAIL);
91+
}
92+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dialog_mode.dart';
6+
import 'dialog_wrapper.dart';
7+
import 'ifile_dialog_controller_factory.dart';
8+
import 'ifile_dialog_factory.dart';
9+
10+
/// Implementation of DialogWrapperFactory that provides [DialogWrapper] instances.
11+
class DialogWrapperFactory {
12+
/// Creates a [DialogWrapperFactory] that makes use of [IFileDialogControllerFactory] and [IFileDialogFactory]
13+
/// to create [DialogWrapper] instances.
14+
DialogWrapperFactory(
15+
this._fileDialogControllerFactory, this._fileDialogFactory);
16+
17+
final IFileDialogControllerFactory _fileDialogControllerFactory;
18+
19+
final IFileDialogFactory _fileDialogFactory;
20+
21+
/// Creates a [DialogWrapper] based on [dialogMode].
22+
DialogWrapper createInstance(DialogMode dialogMode) {
23+
return DialogWrapper(
24+
_fileDialogControllerFactory, _fileDialogFactory, dialogMode);
25+
}
26+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
6+
7+
/// Options for Dialog window
8+
class SelectionOptions {
9+
/// Creates a new [SelectionOptions] instance with the specified values.
10+
/// It defaults [allowMultiple] to false, [selectFolders] to false and no [allowedTypes]
11+
SelectionOptions({
12+
this.allowMultiple = false,
13+
this.selectFolders = false,
14+
this.allowedTypes = const <XTypeGroup>[],
15+
});
16+
17+
/// Indicates whether the user is able to select multiple items at the same time or not.
18+
bool allowMultiple;
19+
20+
/// Indicates whether the user is able to select folders or not.
21+
bool selectFolders;
22+
23+
/// A list of file types that can be selected.
24+
List<XTypeGroup> allowedTypes;
25+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
6+
import 'package:file_selector_windows/src/file_selector_api.dart';
7+
import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart';
8+
import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart';
9+
import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper_factory.dart';
10+
import 'package:file_selector_windows/src/file_selector_dart/selection_options.dart';
11+
import 'package:flutter_test/flutter_test.dart';
12+
import 'package:mockito/annotations.dart';
13+
import 'package:mockito/mockito.dart';
14+
import 'package:win32/win32.dart';
15+
16+
import 'file_selector_api_test.mocks.dart';
17+
18+
@GenerateMocks(<Type>[DialogWrapperFactory, DialogWrapper])
19+
void main() {
20+
const int parentWindow = 1;
21+
final MockDialogWrapperFactory mockDialogWrapperFactory =
22+
MockDialogWrapperFactory();
23+
late MockDialogWrapper mockDialogWrapper;
24+
final FileSelectorApi fileSelectorApi =
25+
FileSelectorApi.useFakeForegroundWindow(
26+
mockDialogWrapperFactory, parentWindow);
27+
28+
const List<String> expectedFileList = <String>['fileA', 'fileB'];
29+
final SelectionOptions emptyOptions = SelectionOptions();
30+
31+
setUp(() {
32+
mockDialogWrapper = MockDialogWrapper();
33+
when(mockDialogWrapper.lastResult).thenReturn(S_OK);
34+
when(mockDialogWrapperFactory.createInstance(DialogMode.Save))
35+
.thenReturn(mockDialogWrapper);
36+
when(mockDialogWrapperFactory.createInstance(DialogMode.Open))
37+
.thenReturn(mockDialogWrapper);
38+
when(mockDialogWrapper.show(parentWindow)).thenReturn(expectedFileList);
39+
});
40+
41+
tearDown(() {
42+
reset(mockDialogWrapper);
43+
reset(mockDialogWrapperFactory);
44+
});
45+
46+
test('FileSelectorApi should not be null', () {
47+
expect(fileSelectorApi, isNotNull);
48+
});
49+
50+
group('showSaveDialog', () {
51+
test('should call setFileName if a suggestedName is provided', () {
52+
// Arrange
53+
const String suggestedName = 'suggestedName';
54+
55+
// Act
56+
fileSelectorApi.showSaveDialog(emptyOptions, null, suggestedName, null);
57+
58+
// Assert
59+
verify(mockDialogWrapper.setFileName(suggestedName)).called(1);
60+
});
61+
62+
test('should create a DialogWrapper with DialogMode Save', () {
63+
// Act
64+
fileSelectorApi.showSaveDialog(emptyOptions, null, null, null);
65+
66+
// Assert
67+
verify(mockDialogWrapperFactory.createInstance(DialogMode.Save))
68+
.called(1);
69+
});
70+
});
71+
group('showOpenDialog', () {
72+
test('should create a DialogWrapper with DialogMode Open', () {
73+
// Act
74+
fileSelectorApi.showOpenDialog(emptyOptions, null, null);
75+
76+
// Assert
77+
verify(mockDialogWrapperFactory.createInstance(DialogMode.Open))
78+
.called(1);
79+
});
80+
});
81+
group('Common behavior', () {
82+
test('should throw a WindowsException is DialogWrapper can not be created',
83+
() {
84+
// Arrange
85+
when(mockDialogWrapperFactory.createInstance(DialogMode.Open))
86+
.thenReturn(mockDialogWrapper);
87+
when(mockDialogWrapper.lastResult).thenReturn(E_FAIL);
88+
89+
// Act - Assert
90+
expect(() => fileSelectorApi.showOpenDialog(emptyOptions, null, null),
91+
throwsA(const TypeMatcher<WindowsException>()));
92+
});
93+
94+
test('should not call AddOptions if no options are configured', () {
95+
// Act
96+
fileSelectorApi.showOpenDialog(emptyOptions, null, null);
97+
98+
// Assert
99+
verifyNever(mockDialogWrapper.addOptions(any));
100+
});
101+
test('should call AddOptions with FOS_PICKFOLDERS configured', () {
102+
// Arrange
103+
final SelectionOptions options = SelectionOptions(selectFolders: true);
104+
105+
// Act
106+
fileSelectorApi.showOpenDialog(options, null, null);
107+
108+
// Assert
109+
verify(mockDialogWrapper
110+
.addOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS))
111+
.called(1);
112+
});
113+
114+
test('should call AddOptions with FOS_ALLOWMULTISELECT configured', () {
115+
// Arrange
116+
final SelectionOptions options = SelectionOptions(allowMultiple: true);
117+
118+
// Act
119+
fileSelectorApi.showOpenDialog(options, null, null);
120+
121+
// Assert
122+
verify(mockDialogWrapper
123+
.addOptions(FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT))
124+
.called(1);
125+
});
126+
127+
test('should call setFolder if an initialDirectory is provided', () {
128+
// Arrange
129+
const String initialDirectory = 'path/to/dir';
130+
131+
// Act
132+
fileSelectorApi.showOpenDialog(emptyOptions, initialDirectory, null);
133+
134+
// Assert
135+
verify(mockDialogWrapper.setFolder(initialDirectory)).called(1);
136+
});
137+
138+
test('should call setOkButtonLabel if confirmButtonText is provided', () {
139+
// Arrange
140+
const String confirmButtonText = 'OK';
141+
142+
// Act
143+
fileSelectorApi.showOpenDialog(emptyOptions, null, confirmButtonText);
144+
145+
// Assert
146+
verify(mockDialogWrapper.setOkButtonLabel(confirmButtonText)).called(1);
147+
});
148+
149+
test('should call setFileTypeFilters with provided allowedTypes', () {
150+
// Arrange
151+
final SelectionOptions options =
152+
SelectionOptions(allowedTypes: <XTypeGroup>[
153+
const XTypeGroup(extensions: <String>['jpg', 'png'], label: 'Images'),
154+
const XTypeGroup(extensions: <String>['txt', 'json'], label: 'Text'),
155+
]);
156+
157+
// Act
158+
fileSelectorApi.showOpenDialog(options, null, null);
159+
160+
// Assert
161+
verify(mockDialogWrapper.setFileTypeFilters(options.allowedTypes))
162+
.called(1);
163+
});
164+
165+
test('should return the file list on success', () {
166+
// Act
167+
final List<String?> result =
168+
fileSelectorApi.showOpenDialog(emptyOptions, null, null);
169+
170+
// Assert
171+
expect(result.length, expectedFileList.length);
172+
expect(result, expectedFileList);
173+
});
174+
175+
test('should throw an exception if file list is null', () {
176+
// Arrange
177+
when(mockDialogWrapper.show(parentWindow)).thenReturn(null);
178+
179+
// Act - Assert
180+
expect(() => fileSelectorApi.showOpenDialog(emptyOptions, null, null),
181+
throwsA(const TypeMatcher<WindowsException>()));
182+
});
183+
});
184+
}

0 commit comments

Comments
 (0)