diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart new file mode 100644 index 000000000000..63b74003e40b --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_api.dart @@ -0,0 +1,92 @@ +// 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/cupertino.dart'; +import 'package:win32/win32.dart'; + +import 'file_selector_dart/dialog_mode.dart'; +import 'file_selector_dart/dialog_wrapper.dart'; +import 'file_selector_dart/dialog_wrapper_factory.dart'; +import 'file_selector_dart/selection_options.dart'; + +/// File dialog handling for Open and Save operations. +class FileSelectorApi { + /// Creates a new instance of [FileSelectorApi]. + /// Allows Dependency Injection of a [DialogWrapperFactory] to handle dialog creation. + FileSelectorApi(this._dialogWrapperFactory) + : _foregroundWindow = GetForegroundWindow(); + + /// Creates a fake instance of [FileSelectorApi] for testing purpose where the [_foregroundWindow] handle is set + /// from the outside. + @visibleForTesting + FileSelectorApi.useFakeForegroundWindow( + this._dialogWrapperFactory, this._foregroundWindow); + + final DialogWrapperFactory _dialogWrapperFactory; + + final int _foregroundWindow; + + /// Displays a dialog window to open one or more files. + List showOpenDialog( + SelectionOptions options, + String? initialDirectory, + String? confirmButtonText, + ) => + _showDialog(_foregroundWindow, DialogMode.Open, options, initialDirectory, + null, confirmButtonText); + + /// Displays a dialog used to save a file. + List showSaveDialog( + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + ) => + _showDialog(_foregroundWindow, DialogMode.Save, options, initialDirectory, + suggestedName, confirmButtonText); + + List _showDialog( + int parentWindow, + DialogMode mode, + SelectionOptions options, + String? initialDirectory, + String? suggestedName, + String? confirmLabel) { + final DialogWrapper dialogWrapper = + _dialogWrapperFactory.createInstance(mode); + if (!SUCCEEDED(dialogWrapper.lastResult)) { + throw WindowsException(E_FAIL); + } + int dialogOptions = 0; + if (options.selectFolders) { + dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS; + } + if (options.allowMultiple) { + dialogOptions |= FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT; + } + if (dialogOptions != 0) { + dialogWrapper.addOptions(dialogOptions); + } + + if (initialDirectory != null) { + dialogWrapper.setFolder(initialDirectory); + } + if (suggestedName != null) { + dialogWrapper.setFileName(suggestedName); + } + if (confirmLabel != null) { + dialogWrapper.setOkButtonLabel(confirmLabel); + } + + if (options.allowedTypes.isNotEmpty) { + dialogWrapper.setFileTypeFilters(options.allowedTypes); + } + + final List? files = dialogWrapper.show(parentWindow); + if (files != null) { + return files; + } + throw WindowsException(E_FAIL); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart new file mode 100644 index 000000000000..ee52efef0a5e --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_mode.dart @@ -0,0 +1,12 @@ +// 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. + +/// The kind of file dialog to show. +enum DialogMode { + /// Used for chosing files. + Open, + + /// Used for chosing a directory to save a file. + Save +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart new file mode 100644 index 000000000000..0f018d49cba1 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper.dart @@ -0,0 +1,197 @@ +// 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 'dart:core'; +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:win32/win32.dart'; + +import 'dialog_mode.dart'; +import 'file_dialog_controller.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_dialog_factory.dart'; +import 'shell_win32_api.dart'; + +/// Wraps an IFileDialog, managing object lifetime as a scoped object and +/// providing a simplified API for interacting with it as needed for the plugin. +class DialogWrapper { + /// Creates a DialogWrapper using a [IFileDialogControllerFactory] and a [DialogMode]. + /// It is also responsible of creating a [IFileDialog]. + DialogWrapper(IFileDialogControllerFactory fileDialogControllerFactory, + IFileDialogFactory fileDialogFactory, this._dialogMode) + : _isOpenDialog = _dialogMode == DialogMode.Open { + try { + final IFileDialog dialog = fileDialogFactory.createInstace(_dialogMode); + _dialogController = fileDialogControllerFactory.createController(dialog); + _shellWin32Api = ShellWin32Api(); + } catch (ex) { + if (ex is WindowsException) { + _lastResult = ex.hr; + } + } + } + + /// Creates a DialogWrapper for testing purposes. + @visibleForTesting + DialogWrapper.withFakeDependencies(FileDialogController dialogController, + this._dialogMode, this._shellWin32Api) + : _isOpenDialog = _dialogMode == DialogMode.Open, + _dialogController = dialogController; + + int _lastResult = S_OK; + + final DialogMode _dialogMode; + + final bool _isOpenDialog; + + final String _allowAnyValue = 'Any'; + + final String _allowAnyExtension = '*.*'; + + late FileDialogController _dialogController; + + late ShellWin32Api _shellWin32Api; + + /// Returns the result of the last Win32 API call related to this object. + int get lastResult => _lastResult; + + /// Attempts to set the default folder for the dialog to [path], if it exists. + void setFolder(String path) { + if (path == null || path.isEmpty) { + return; + } + + using((Arena arena) { + final Pointer ptrGuid = GUIDFromString(IID_IShellItem); + final Pointer> ptrPath = arena>(); + _lastResult = + _shellWin32Api.createItemFromParsingName(path, ptrGuid, ptrPath); + + if (!SUCCEEDED(_lastResult)) { + return; + } + + _dialogController.setFolder(ptrPath.value); + }); + } + + /// Sets the file name that is initially shown in the dialog. + void setFileName(String name) { + _dialogController.setFileName(name); + } + + /// Sets the label of the confirmation button. + void setOkButtonLabel(String label) { + _dialogController.setOkButtonLabel(label); + } + + /// Adds the given options to the dialog's current [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html). + /// Both are bitfields. + void addOptions(int newOptions) { + using((Arena arena) { + final Pointer currentOptions = arena(); + _lastResult = _dialogController.getOptions(currentOptions); + if (!SUCCEEDED(_lastResult)) { + return; + } + currentOptions.value |= newOptions; + _lastResult = _dialogController.setOptions(currentOptions.value); + }); + } + + /// Sets the filters for allowed file types to select. + /// filters -> std::optional + void setFileTypeFilters(List filters) { + final Map filterSpecification = {}; + + if (filters.isEmpty) { + filterSpecification[_allowAnyValue] = _allowAnyExtension; + } else { + for (final XTypeGroup option in filters) { + final String? label = option.label; + if (option.allowsAny || option.extensions!.isEmpty) { + filterSpecification[label ?? _allowAnyValue] = _allowAnyExtension; + } else { + final String extensionsForLabel = option.extensions! + .map((String extension) => '*.$extension') + .join(';'); + filterSpecification[label ?? extensionsForLabel] = extensionsForLabel; + } + } + } + + using((Arena arena) { + final Pointer registerFilterSpecification = + arena(filterSpecification.length); + + int index = 0; + for (final String key in filterSpecification.keys) { + registerFilterSpecification[index] + ..pszName = TEXT(key) + ..pszSpec = TEXT(filterSpecification[key]!); + index++; + } + + _lastResult = _dialogController.setFileTypes( + filterSpecification.length, registerFilterSpecification); + }); + } + + /// Displays the dialog, and returns the selected files, or null on error. + List? show(int parentWindow) { + _lastResult = _dialogController.show(parentWindow); + if (!SUCCEEDED(_lastResult)) { + return null; + } + late List? files; + + using((Arena arena) { + final Pointer> shellItemArrayPtr = + arena>(); + final Pointer shellItemCountPtr = arena(); + final Pointer> shellItemPtr = + arena>(); + + files = + _getFilePathList(shellItemArrayPtr, shellItemCountPtr, shellItemPtr); + }); + return files; + } + + List? _getFilePathList( + Pointer> shellItemArrayPtr, + Pointer shellItemCountPtr, + Pointer> shellItemPtr) { + final List files = []; + if (_isOpenDialog) { + _lastResult = _dialogController.getResults(shellItemArrayPtr); + if (!SUCCEEDED(_lastResult)) { + return null; + } + + final IShellItemArray shellItemResources = + IShellItemArray(shellItemArrayPtr.cast()); + _lastResult = shellItemResources.getCount(shellItemCountPtr); + if (!SUCCEEDED(_lastResult)) { + return null; + } + for (int index = 0; index < shellItemCountPtr.value; index++) { + shellItemResources.getItemAt(index, shellItemPtr); + final IShellItem shellItem = IShellItem(shellItemPtr.cast()); + files.add(_shellWin32Api.getPathForShellItem(shellItem)); + } + } else { + _lastResult = _dialogController.getResult(shellItemPtr); + if (!SUCCEEDED(_lastResult)) { + return null; + } + final IShellItem shellItem = IShellItem(shellItemPtr.cast()); + files.add(_shellWin32Api.getPathForShellItem(shellItem)); + } + return files; + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart new file mode 100644 index 000000000000..cb75c7b432d4 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/dialog_wrapper_factory.dart @@ -0,0 +1,26 @@ +// 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 'dialog_mode.dart'; +import 'dialog_wrapper.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_dialog_factory.dart'; + +/// Implementation of DialogWrapperFactory that provides [DialogWrapper] instances. +class DialogWrapperFactory { + /// Creates a [DialogWrapperFactory] that makes use of [IFileDialogControllerFactory] and [IFileDialogFactory] + /// to create [DialogWrapper] instances. + DialogWrapperFactory( + this._fileDialogControllerFactory, this._fileDialogFactory); + + final IFileDialogControllerFactory _fileDialogControllerFactory; + + final IFileDialogFactory _fileDialogFactory; + + /// Creates a [DialogWrapper] based on [dialogMode]. + DialogWrapper createInstance(DialogMode dialogMode) { + return DialogWrapper( + _fileDialogControllerFactory, _fileDialogFactory, dialogMode); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart new file mode 100644 index 000000000000..65164eb9e40e --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart @@ -0,0 +1,91 @@ +// 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 'dart:ffi'; + +import 'package:win32/win32.dart'; + +import 'ifile_open_dialog_factory.dart'; + +/// A thin wrapper for IFileDialog to allow for faking and inspection in tests. +/// +/// Since this class defines the end of what can be unit tested, it should +/// contain as little logic as possible. +class FileDialogController { + /// Creates a controller managing [IFileDialog](https://pub.dev/documentation/win32/latest/winrt/IFileDialog-class.html). + /// It also receives an IFileOpenDialogFactory to construct [IFileOpenDialog] + /// instances. + FileDialogController( + IFileDialog fileDialog, IFileOpenDialogFactory iFileOpenDialogFactory) + : _fileDialog = fileDialog, + _iFileOpenDialogFactory = iFileOpenDialogFactory; + + /// The [IFileDialog] to work with. + final IFileDialog _fileDialog; + + /// The [IFileOpenDialogFactory] to work construc [IFileOpenDialog] instances. + final IFileOpenDialogFactory _iFileOpenDialogFactory; + + /// Sets the default folder for the dialog to [path]. It also returns the operation result. + int setFolder(Pointer path) { + return _fileDialog.setFolder(path); + } + + /// Sets the file [name] that is initially shown in the IFileDialog. It also returns the operation result. + int setFileName(String name) { + return _fileDialog.setFileName(TEXT(name)); + } + + /// Sets the allowed file type extensions in the IFileOpenDialog. It also returns the operation result. + int setFileTypes(int count, Pointer filters) { + return _fileDialog.setFileTypes(count, filters); + } + + /// Sets the label of the confirmation button. It also returns the operation result. It also returns the operation result. + int setOkButtonLabel(String text) { + return _fileDialog.setOkButtonLabel(TEXT(text)); + } + + /// Gets the IFileDialog's [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html), + /// which is a bitfield. It also returns the operation result. + int getOptions(Pointer outOptions) { + return _fileDialog.getOptions(outOptions); + } + + /// Sets the [options](https://pub.dev/documentation/win32/latest/winrt/FILEOPENDIALOGOPTIONS-class.html), + /// which is a bitfield, into the IFileDialog. It also returns the operation result. + int setOptions(int options) { + return _fileDialog.setOptions(options); + } + + /// Shows an IFileDialog using the given parent. It returns the operation result. + int show(int parent) { + return _fileDialog.show(parent); + } + + /// Return results from an IFileDialog. This should be used when selecting + /// single items. It also returns the operation result. + int getResult(Pointer> outItem) { + return _fileDialog.getResult(outItem); + } + + /// Return results from an IFileOpenDialog. This should be used when selecting + /// single items. This function will fail if the IFileDialog* provided to the + /// constructor was not an IFileOpenDialog instance, returning an E_FAIL + /// error. + int getResults(Pointer> outItems) { + IFileOpenDialog? fileOpenDialog; + try { + fileOpenDialog = _iFileOpenDialogFactory.from(_fileDialog); + return fileOpenDialog.getResults(outItems); + } catch (_) { + return E_FAIL; + } finally { + fileOpenDialog?.release(); + if (fileOpenDialog != null) { + free(fileOpenDialog.ptr); + } + } + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart new file mode 100644 index 000000000000..296c9ba4529f --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller_factory.dart @@ -0,0 +1,18 @@ +// 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:win32/win32.dart'; + +import 'file_dialog_controller.dart'; +import 'ifile_dialog_controller_factory.dart'; +import 'ifile_open_dialog_factory.dart'; + +/// Implementation of FileDialogControllerFactory that makes standard +/// FileDialogController instances. +class FileDialogControllerFactory implements IFileDialogControllerFactory { + @override + FileDialogController createController(IFileDialog dialog) { + return FileDialogController(dialog, IFileOpenDialogFactory()); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.dart new file mode 100644 index 000000000000..63f081b82944 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_controller_factory.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:win32/win32.dart'; + +import 'file_dialog_controller.dart'; + +/// Interface for creating FileDialogControllers, to allow for dependency +/// injection. +abstract class IFileDialogControllerFactory { + /// Returns a FileDialogController to interact with the given [IFileDialog]. + FileDialogController createController(IFileDialog dialog); +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart new file mode 100644 index 000000000000..4a13d8731a33 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_dialog_factory.dart @@ -0,0 +1,19 @@ +// 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:win32/win32.dart'; + +import 'dialog_mode.dart'; + +/// A factory for [IFileDialog] instances. +class IFileDialogFactory { + /// Creates the corresponding IFileDialog instace. The caller is responsible of releasing the resource. + IFileDialog createInstace(DialogMode dialogMode) { + if (dialogMode == DialogMode.Open) { + return FileOpenDialog.createInstance(); + } + + return FileSaveDialog.createInstance(); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart new file mode 100644 index 000000000000..fca0095db413 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart @@ -0,0 +1,13 @@ +// 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:win32/win32.dart'; + +/// A wrapper of the IFileOpenDialog interface to use its from function. +class IFileOpenDialogFactory { + /// Wraps the IFileOpenDialog from function. + IFileOpenDialog from(IFileDialog fileDialog) { + return IFileOpenDialog.from(fileDialog); + } +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.dart new file mode 100644 index 000000000000..d00e30198b1d --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/selection_options.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:file_selector_platform_interface/file_selector_platform_interface.dart'; + +/// Options for Dialog window +class SelectionOptions { + /// Creates a new [SelectionOptions] instance with the specified values. + /// It defaults [allowMultiple] to false, [selectFolders] to false and no [allowedTypes] + SelectionOptions({ + this.allowMultiple = false, + this.selectFolders = false, + this.allowedTypes = const [], + }); + + /// Indicates whether the user is able to select multiple items at the same time or not. + bool allowMultiple; + + /// Indicates whether the user is able to select folders or not. + bool selectFolders; + + /// A list of file types that can be selected. + List allowedTypes; +} diff --git a/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart new file mode 100644 index 000000000000..4390e6580aa0 --- /dev/null +++ b/packages/file_selector/file_selector_windows/lib/src/file_selector_dart/shell_win32_api.dart @@ -0,0 +1,36 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +/// A thin wrapper for Win32 platform specific Shell methods. +/// +/// The only purpose of this class is to decouple specific Win32 Api call from the bussiness logic so it can be init tested in any environment. +class ShellWin32Api { + /// Creates and [initializes](https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-shcreateitemfromparsingname) a Shell item object from a parsing name. + /// If the directory doesn't exist it will return an error result. + int createItemFromParsingName(String initialDirectory, Pointer ptrGuid, + Pointer> ptrPath) { + return SHCreateItemFromParsingName( + TEXT(initialDirectory), nullptr, ptrGuid, ptrPath); + } + + /// Returns the path for [shellItem] as a UTF-8 string, or an empty string on + /// failure. + String getPathForShellItem(IShellItem shellItem) { + return using((Arena arena) { + final Pointer> ptrPath = arena>(); + + if (!SUCCEEDED( + shellItem.getDisplayName(SIGDN.SIGDN_FILESYSPATH, ptrPath.cast()))) { + return ''; + } + + return ptrPath.value.toDartString(); + }); + } +} diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index ee0701b3fd30..ef6d25d8f8de 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -18,9 +18,11 @@ flutter: dependencies: cross_file: ^0.3.1 + ffi: ^2.0.1 file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter + win32: ^3.0.0 dev_dependencies: build_runner: 2.1.11 diff --git a/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart new file mode 100644 index 000000000000..53aec2d5b1de --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.dart @@ -0,0 +1,184 @@ +// 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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_windows/src/file_selector_api.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper_factory.dart'; +import 'package:file_selector_windows/src/file_selector_dart/selection_options.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:win32/win32.dart'; + +import 'file_selector_api_test.mocks.dart'; + +@GenerateMocks([DialogWrapperFactory, DialogWrapper]) +void main() { + const int parentWindow = 1; + final MockDialogWrapperFactory mockDialogWrapperFactory = + MockDialogWrapperFactory(); + late MockDialogWrapper mockDialogWrapper; + final FileSelectorApi fileSelectorApi = + FileSelectorApi.useFakeForegroundWindow( + mockDialogWrapperFactory, parentWindow); + + const List expectedFileList = ['fileA', 'fileB']; + final SelectionOptions emptyOptions = SelectionOptions(); + + setUp(() { + mockDialogWrapper = MockDialogWrapper(); + when(mockDialogWrapper.lastResult).thenReturn(S_OK); + when(mockDialogWrapperFactory.createInstance(DialogMode.Save)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapperFactory.createInstance(DialogMode.Open)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapper.show(parentWindow)).thenReturn(expectedFileList); + }); + + tearDown(() { + reset(mockDialogWrapper); + reset(mockDialogWrapperFactory); + }); + + test('FileSelectorApi should not be null', () { + expect(fileSelectorApi, isNotNull); + }); + + group('showSaveDialog', () { + test('should call setFileName if a suggestedName is provided', () { + // Arrange + const String suggestedName = 'suggestedName'; + + // Act + fileSelectorApi.showSaveDialog(emptyOptions, null, suggestedName, null); + + // Assert + verify(mockDialogWrapper.setFileName(suggestedName)).called(1); + }); + + test('should create a DialogWrapper with DialogMode Save', () { + // Act + fileSelectorApi.showSaveDialog(emptyOptions, null, null, null); + + // Assert + verify(mockDialogWrapperFactory.createInstance(DialogMode.Save)) + .called(1); + }); + }); + group('showOpenDialog', () { + test('should create a DialogWrapper with DialogMode Open', () { + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + verify(mockDialogWrapperFactory.createInstance(DialogMode.Open)) + .called(1); + }); + }); + group('Common behavior', () { + test('should throw a WindowsException is DialogWrapper can not be created', + () { + // Arrange + when(mockDialogWrapperFactory.createInstance(DialogMode.Open)) + .thenReturn(mockDialogWrapper); + when(mockDialogWrapper.lastResult).thenReturn(E_FAIL); + + // Act - Assert + expect(() => fileSelectorApi.showOpenDialog(emptyOptions, null, null), + throwsA(const TypeMatcher())); + }); + + test('should not call AddOptions if no options are configured', () { + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + verifyNever(mockDialogWrapper.addOptions(any)); + }); + test('should call AddOptions with FOS_PICKFOLDERS configured', () { + // Arrange + final SelectionOptions options = SelectionOptions(selectFolders: true); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper + .addOptions(FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS)) + .called(1); + }); + + test('should call AddOptions with FOS_ALLOWMULTISELECT configured', () { + // Arrange + final SelectionOptions options = SelectionOptions(allowMultiple: true); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper + .addOptions(FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT)) + .called(1); + }); + + test('should call setFolder if an initialDirectory is provided', () { + // Arrange + const String initialDirectory = 'path/to/dir'; + + // Act + fileSelectorApi.showOpenDialog(emptyOptions, initialDirectory, null); + + // Assert + verify(mockDialogWrapper.setFolder(initialDirectory)).called(1); + }); + + test('should call setOkButtonLabel if confirmButtonText is provided', () { + // Arrange + const String confirmButtonText = 'OK'; + + // Act + fileSelectorApi.showOpenDialog(emptyOptions, null, confirmButtonText); + + // Assert + verify(mockDialogWrapper.setOkButtonLabel(confirmButtonText)).called(1); + }); + + test('should call setFileTypeFilters with provided allowedTypes', () { + // Arrange + final SelectionOptions options = + SelectionOptions(allowedTypes: [ + const XTypeGroup(extensions: ['jpg', 'png'], label: 'Images'), + const XTypeGroup(extensions: ['txt', 'json'], label: 'Text'), + ]); + + // Act + fileSelectorApi.showOpenDialog(options, null, null); + + // Assert + verify(mockDialogWrapper.setFileTypeFilters(options.allowedTypes)) + .called(1); + }); + + test('should return the file list on success', () { + // Act + final List result = + fileSelectorApi.showOpenDialog(emptyOptions, null, null); + + // Assert + expect(result.length, expectedFileList.length); + expect(result, expectedFileList); + }); + + test('should throw an exception if file list is null', () { + // Arrange + when(mockDialogWrapper.show(parentWindow)).thenReturn(null); + + // Act - Assert + expect(() => fileSelectorApi.showOpenDialog(emptyOptions, null, null), + throwsA(const TypeMatcher())); + }); + }); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart new file mode 100644 index 000000000000..e6544d2dda27 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_api_test.mocks.dart @@ -0,0 +1,122 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_windows/example/windows/flutter/ephemeral/.plugin_symlinks/file_selector_windows/test/file_selector_api_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i5; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart' + as _i4; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart' + as _i2; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper_factory.dart' + as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeDialogWrapper_0 extends _i1.SmartFake implements _i2.DialogWrapper { + _FakeDialogWrapper_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [DialogWrapperFactory]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDialogWrapperFactory extends _i1.Mock + implements _i3.DialogWrapperFactory { + MockDialogWrapperFactory() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.DialogWrapper createInstance(_i4.DialogMode? dialogMode) => + (super.noSuchMethod( + Invocation.method( + #createInstance, + [dialogMode], + ), + returnValue: _FakeDialogWrapper_0( + this, + Invocation.method( + #createInstance, + [dialogMode], + ), + ), + ) as _i2.DialogWrapper); +} + +/// A class which mocks [DialogWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDialogWrapper extends _i1.Mock implements _i2.DialogWrapper { + MockDialogWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + int get lastResult => (super.noSuchMethod( + Invocation.getter(#lastResult), + returnValue: 0, + ) as int); + @override + void setFolder(String? path) => super.noSuchMethod( + Invocation.method( + #setFolder, + [path], + ), + returnValueForMissingStub: null, + ); + @override + void setFileName(String? name) => super.noSuchMethod( + Invocation.method( + #setFileName, + [name], + ), + returnValueForMissingStub: null, + ); + @override + void setOkButtonLabel(String? label) => super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [label], + ), + returnValueForMissingStub: null, + ); + @override + void addOptions(int? newOptions) => super.noSuchMethod( + Invocation.method( + #addOptions, + [newOptions], + ), + returnValueForMissingStub: null, + ); + @override + void setFileTypeFilters(List<_i5.XTypeGroup>? filters) => super.noSuchMethod( + Invocation.method( + #setFileTypeFilters, + [filters], + ), + returnValueForMissingStub: null, + ); + @override + List? show(int? parentWindow) => + (super.noSuchMethod(Invocation.method( + #show, + [parentWindow], + )) as List?); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart new file mode 100644 index 000000000000..056075cbee02 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart @@ -0,0 +1,306 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_mode.dart'; +import 'package:file_selector_windows/src/file_selector_dart/dialog_wrapper.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:file_selector_windows/src/file_selector_dart/shell_win32_api.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:win32/win32.dart'; + +import 'dialog_wrapper_test.mocks.dart'; + +@GenerateMocks([FileDialogController, ShellWin32Api]) +void main() { + const int defaultReturnValue = S_OK; + late final MockFileDialogController mockFileDialogController = + MockFileDialogController(); + late final MockShellWin32Api mockShellWin32Api = MockShellWin32Api(); + const DialogMode dialogMode = DialogMode.Open; + final DialogWrapper dialogWrapper = DialogWrapper.withFakeDependencies( + mockFileDialogController, dialogMode, mockShellWin32Api); + + setUp(() { + setDefaultMocks(mockFileDialogController, defaultReturnValue); + }); + + tearDown(() { + reset(mockFileDialogController); + reset(mockShellWin32Api); + }); + + test('setFileName should call dialog setFileName', () { + const String folderName = 'Documents'; + dialogWrapper.setFileName(folderName); + verify(mockFileDialogController.setFileName(folderName)).called(1); + }); + + test('setOkButtonLabel should call dialog setOkButtonLabel', () { + const String okButtonLabel = 'Confirm'; + dialogWrapper.setOkButtonLabel(okButtonLabel); + verify(mockFileDialogController.setOkButtonLabel(okButtonLabel)).called(1); + }); + + test('addOptions should call dialog getOptions and setOptions', () { + const int newOptions = FILEOPENDIALOGOPTIONS.FOS_NOREADONLYRETURN; + dialogWrapper.addOptions(newOptions); + verify(mockFileDialogController.getOptions(any)).called(1); + verify(mockFileDialogController.setOptions(newOptions)).called(1); + }); + + test('addOptions should not call setOptions if getOptions returns an error', + () { + const int options = FILEOPENDIALOGOPTIONS.FOS_NOREADONLYRETURN; + when(mockFileDialogController.getOptions(any)).thenReturn(E_FAIL); + dialogWrapper.addOptions(options); + verifyNever(mockFileDialogController.setOptions(any)); + }); + + test( + 'setFileTypeFilters should call setFileTypes with expected typeGroups count', + () { + final List typeGroups = [ + const XTypeGroup(extensions: ['jpg', 'png'], label: 'Images'), + const XTypeGroup(extensions: ['txt', 'json'], label: 'Text'), + ]; + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + }); + + test('setFileTypeFilters should call setFileTypes with Any by default', () { + const String expectedPszName = 'Any'; + const String expectedPszSpec = '*.*'; + final List typeGroups = []; + mockSetFileTypesConditions( + mockFileDialogController, expectedPszName, expectedPszSpec); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with a label and default extensions', + () { + const String label = 'All files'; + const String expectedPszSpec = '*.*'; + final List typeGroups = [ + const XTypeGroup(label: label), + ]; + mockSetFileTypesConditions( + mockFileDialogController, label, expectedPszSpec); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with both default label and extensions', + () { + const String defaultLabel = 'Any'; + const String expectedPszSpec = '*.*'; + final List typeGroups = [ + const XTypeGroup(), + ]; + mockSetFileTypesConditions( + mockFileDialogController, defaultLabel, expectedPszSpec); + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(1, any)).called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with specific labels and extensions', + () { + const String jpg = 'jpg'; + const String png = 'png'; + const String imageLabel = 'Image'; + const String txt = 'txt'; + const String json = 'json'; + const String textLabel = 'Text'; + final Map expectedfilterSpecification = { + imageLabel: '*.$jpg;*.$png', + textLabel: '*.$txt;*.$json', + }; + final List typeGroups = [ + const XTypeGroup(extensions: [jpg, png], label: imageLabel), + const XTypeGroup(extensions: [txt, json], label: textLabel), + ]; + when(mockFileDialogController.setFileTypes(any, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + int index = 0; + for (final String key in expectedfilterSpecification.keys) { + if (pointer[index].pszName.toDartString() != key || + pointer[index].pszSpec.toDartString() != + expectedfilterSpecification[key]) { + return E_FAIL; + } + index++; + } + return S_OK; + }); + + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test( + 'setFileTypeFilters should call setFileTypes with specific extensions and No label', + () { + const String jpg = 'jpg'; + const String png = 'png'; + const String txt = 'txt'; + const String json = 'json'; + final Map expectedfilterSpecification = { + '*.$jpg;*.$png': '*.$jpg;*.$png', + '*.$txt;*.$json': '*.$txt;*.$json', + }; + final List typeGroups = [ + const XTypeGroup(extensions: [jpg, png]), + const XTypeGroup(extensions: [txt, json]), + ]; + when(mockFileDialogController.setFileTypes(any, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + int index = 0; + for (final String key in expectedfilterSpecification.keys) { + if (pointer[index].pszName.toDartString() != key || + pointer[index].pszSpec.toDartString() != + expectedfilterSpecification[key]) { + return E_FAIL; + } + index++; + } + return S_OK; + }); + + dialogWrapper.setFileTypeFilters(typeGroups); + verify(mockFileDialogController.setFileTypes(typeGroups.length, any)) + .called(1); + expect(dialogWrapper.lastResult, S_OK); + }); + + test('setFolder should not call dialog setFolder if the path is empty', () { + const String emptyPath = ''; + dialogWrapper.setFolder(emptyPath); + verifyNever(mockFileDialogController.setFolder(any)); + }); + + test('setFolder should call dialog setFolder with the provided path', () { + const String path = 'path/to/my/folder'; + when(mockShellWin32Api.createItemFromParsingName(path, any, any)) + .thenReturn(S_OK); + dialogWrapper.setFolder(path); + verify(mockFileDialogController.setFolder(any)).called(1); + }); + + test('setFolder should not call dialog setFolder if createItem fails', () { + const String path = 'path/to/my/folder'; + when(mockShellWin32Api.createItemFromParsingName(path, any, any)) + .thenReturn(E_FAIL); + dialogWrapper.setFolder(path); + verifyNever(mockFileDialogController.setFolder(any)); + }); + + test( + '[DialogMode == Open] show should return null if parent window is not available', + () { + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(E_FAIL); + + final List? result = dialogWrapper.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verifyNever(mockFileDialogController.getResults(any)); + }); + + test( + "[DialogMode == Open] show should return null if can't get results from dialog", + () { + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResults(any)).thenReturn(E_FAIL); + + final List? result = dialogWrapper.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verify(mockFileDialogController.getResults(any)).called(1); + }); + + test( + "[DialogMode == Save] show should return null if can't get result from dialog", + () { + final DialogWrapper dialogWrapperModeSave = + DialogWrapper.withFakeDependencies( + mockFileDialogController, DialogMode.Save, mockShellWin32Api); + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResult(any)).thenReturn(E_FAIL); + + final List? result = dialogWrapperModeSave.show(parentWindow); + + expect(result, null); + verify(mockFileDialogController.show(parentWindow)).called(1); + verify(mockFileDialogController.getResult(any)).called(1); + }); + + test('[DialogMode == Save] show should the selected directory for', () { + const String filePath = 'path/to/file.txt'; + final DialogWrapper dialogWrapperModeSave = + DialogWrapper.withFakeDependencies( + mockFileDialogController, DialogMode.Save, mockShellWin32Api); + const int parentWindow = 0; + when(mockFileDialogController.show(parentWindow)).thenReturn(S_OK); + when(mockFileDialogController.getResult(any)).thenReturn(S_OK); + when(mockShellWin32Api.getPathForShellItem(any)).thenReturn(filePath); + + final List? result = dialogWrapperModeSave.show(parentWindow); + + expect(result?.first, filePath); + }); +} + +void mockSetFileTypesConditions( + MockFileDialogController mockFileDialogController, + String expectedPszName, + String expectedPszSpec) { + when(mockFileDialogController.setFileTypes(1, any)) + .thenAnswer((Invocation realInvocation) { + final Pointer pointer = + realInvocation.positionalArguments[1] as Pointer; + + return pointer[0].pszName.toDartString() == expectedPszName && + pointer[0].pszSpec.toDartString() == expectedPszSpec + ? S_OK + : E_FAIL; + }); +} + +void setDefaultMocks( + MockFileDialogController mockFileDialogController, int defaultReturnValue) { + when(mockFileDialogController.setOptions(any)).thenReturn(defaultReturnValue); + when(mockFileDialogController.getOptions(any)).thenReturn(defaultReturnValue); + when(mockFileDialogController.setOkButtonLabel(any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFileName(any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFileTypes(any, any)) + .thenReturn(defaultReturnValue); + when(mockFileDialogController.setFolder(any)).thenReturn(defaultReturnValue); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart new file mode 100644 index 000000000000..5227cc12b188 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/dialog_wrapper_test.mocks.dart @@ -0,0 +1,151 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_windows/test/file_selector_dart/dialog_wrapper_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ffi' as _i3; + +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart' + as _i2; +import 'package:file_selector_windows/src/file_selector_dart/shell_win32_api.dart' + as _i5; +import 'package:mockito/mockito.dart' as _i1; +import 'package:win32/win32.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileDialogController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileDialogController extends _i1.Mock + implements _i2.FileDialogController { + MockFileDialogController() { + _i1.throwOnMissingStub(this); + } + + @override + int setFolder(_i3.Pointer<_i4.COMObject>? path) => (super.noSuchMethod( + Invocation.method( + #setFolder, + [path], + ), + returnValue: 0, + ) as int); + @override + int setFileName(String? name) => (super.noSuchMethod( + Invocation.method( + #setFileName, + [name], + ), + returnValue: 0, + ) as int); + @override + int setFileTypes( + int? count, + _i3.Pointer<_i4.COMDLG_FILTERSPEC>? filters, + ) => + (super.noSuchMethod( + Invocation.method( + #setFileTypes, + [ + count, + filters, + ], + ), + returnValue: 0, + ) as int); + @override + int setOkButtonLabel(String? text) => (super.noSuchMethod( + Invocation.method( + #setOkButtonLabel, + [text], + ), + returnValue: 0, + ) as int); + @override + int getOptions(_i3.Pointer<_i3.Uint32>? outOptions) => (super.noSuchMethod( + Invocation.method( + #getOptions, + [outOptions], + ), + returnValue: 0, + ) as int); + @override + int setOptions(int? options) => (super.noSuchMethod( + Invocation.method( + #setOptions, + [options], + ), + returnValue: 0, + ) as int); + @override + int show(int? parent) => (super.noSuchMethod( + Invocation.method( + #show, + [parent], + ), + returnValue: 0, + ) as int); + @override + int getResult(_i3.Pointer<_i3.Pointer<_i4.COMObject>>? outItem) => + (super.noSuchMethod( + Invocation.method( + #getResult, + [outItem], + ), + returnValue: 0, + ) as int); + @override + int getResults(_i3.Pointer<_i3.Pointer<_i4.COMObject>>? outItems) => + (super.noSuchMethod( + Invocation.method( + #getResults, + [outItems], + ), + returnValue: 0, + ) as int); +} + +/// A class which mocks [ShellWin32Api]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockShellWin32Api extends _i1.Mock implements _i5.ShellWin32Api { + MockShellWin32Api() { + _i1.throwOnMissingStub(this); + } + + @override + int createItemFromParsingName( + String? initialDirectory, + _i3.Pointer<_i4.GUID>? ptrGuid, + _i3.Pointer<_i3.Pointer<_i3.NativeType>>? ptrPath, + ) => + (super.noSuchMethod( + Invocation.method( + #createItemFromParsingName, + [ + initialDirectory, + ptrGuid, + ptrPath, + ], + ), + returnValue: 0, + ) as int); + @override + String getPathForShellItem(_i4.IShellItem? shellItem) => (super.noSuchMethod( + Invocation.method( + #getPathForShellItem, + [shellItem], + ), + returnValue: '', + ) as String); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart new file mode 100644 index 000000000000..84a6713868a6 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart @@ -0,0 +1,112 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IFileDialog class for testing purposes. +class FakeIFileDialog extends Fake implements IFileDialog { + int _getOptionsCalledTimes = 0; + int _getResultCalledTimes = 0; + int _setOptionsCalledTimes = 0; + int _setFolderCalledTimes = 0; + int _setFileNameCalledTimes = 0; + int _setFileTypesCalledTimes = 0; + int _setOkButtonLabelCalledTimes = 0; + int _showCalledTimes = 0; + + @override + int getOptions(Pointer pfos) { + _getOptionsCalledTimes++; + return S_OK; + } + + @override + int setOptions(int options) { + _setOptionsCalledTimes++; + return S_OK; + } + + @override + int getResult(Pointer> ppsi) { + _getResultCalledTimes++; + return S_OK; + } + + @override + int setFolder(Pointer psi) { + _setFolderCalledTimes++; + return S_OK; + } + + @override + int setFileTypes(int cFileTypes, Pointer rgFilterSpec) { + _setFileTypesCalledTimes++; + return S_OK; + } + + @override + int setFileName(Pointer pszName) { + _setFileNameCalledTimes++; + return S_OK; + } + + @override + int setOkButtonLabel(Pointer pszText) { + _setOkButtonLabelCalledTimes++; + return S_OK; + } + + @override + int show(int hwndOwner) { + _showCalledTimes++; + return S_OK; + } + + void resetCounters() { + _getOptionsCalledTimes = 0; + _getResultCalledTimes = 0; + _setOptionsCalledTimes = 0; + _setFolderCalledTimes = 0; + _setFileTypesCalledTimes = 0; + _setOkButtonLabelCalledTimes = 0; + _showCalledTimes = 0; + _setFileNameCalledTimes = 0; + } + + int getOptionsCalledTimes() { + return _getOptionsCalledTimes; + } + + int setOptionsCalledTimes() { + return _setOptionsCalledTimes; + } + + int getResultCalledTimes() { + return _getResultCalledTimes; + } + + int setFolderCalledTimes() { + return _setFolderCalledTimes; + } + + int setFileNameCalledTimes() { + return _setFileNameCalledTimes; + } + + int setFileTypesCalledTimes() { + return _setFileTypesCalledTimes; + } + + int setOkButtonLabelCalledTimes() { + return _setOkButtonLabelCalledTimes; + } + + int showCalledTimes() { + return _showCalledTimes; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart new file mode 100644 index 000000000000..14dd9eb9f215 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart @@ -0,0 +1,55 @@ +// 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 'dart:ffi'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +// Fake IFileOpenDialog class for testing purposes. +class FakeIFileOpenDialog extends Fake implements IFileOpenDialog { + int _getResultsCalledTimes = 0; + int _getReleaseCalledTimes = 0; + bool _shouldFail = false; + + @override + Pointer get ptr => nullptr; + + @override + int release() { + _getReleaseCalledTimes++; + return S_OK; + } + + @override + int getResults(Pointer> ppsi) { + _getResultsCalledTimes++; + if (_shouldFail) { + throw WindowsException(E_FAIL); + } + + return S_OK; + } + + void resetCounters() { + _getResultsCalledTimes = 0; + _getReleaseCalledTimes = 0; + } + + int getResultsCalledTimes() { + return _getResultsCalledTimes; + } + + int getReleaseCalledTimes() { + return _getReleaseCalledTimes; + } + + void mockFailure() { + _shouldFail = true; + } + + void mockSuccess() { + _shouldFail = false; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart new file mode 100644 index 000000000000..042a14f56d5a --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart @@ -0,0 +1,40 @@ +// 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:file_selector_windows/src/file_selector_dart/ifile_open_dialog_factory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_ifile_open_dialog.dart'; + +// Fake FakeIFileOpenDialogFactory class for testing purposes. +class FakeIFileOpenDialogFactory extends Fake + implements IFileOpenDialogFactory { + int _fromCalledTimes = 0; + bool _shouldFail = false; + + final FakeIFileOpenDialog fakeIFileOpenDialog = FakeIFileOpenDialog(); + + @override + IFileOpenDialog from(IFileDialog dialog) { + _fromCalledTimes++; + if (_shouldFail) { + throw WindowsException(E_NOINTERFACE); + } + + return fakeIFileOpenDialog; + } + + int getFromCalledTimes() { + return _fromCalledTimes; + } + + void mockSuccess() { + _shouldFail = false; + } + + void mockFailure() { + _shouldFail = true; + } +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart new file mode 100644 index 000000000000..fe100ecdebca --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_factory_test.dart @@ -0,0 +1,21 @@ +// 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:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller_factory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_file_dialog.dart'; + +void main() { + final FileDialogControllerFactory fileDialogControllerFactory = + FileDialogControllerFactory(); + final IFileDialog dialog = FakeIFileDialog(); + + test('createController should return a FileDialogController', () { + expect(fileDialogControllerFactory.createController(dialog), + isA()); + }); +} diff --git a/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart new file mode 100644 index 000000000000..719a9ad74277 --- /dev/null +++ b/packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart @@ -0,0 +1,131 @@ +// 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 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:file_selector_windows/src/file_selector_dart/file_dialog_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:win32/win32.dart'; + +import 'fake_file_dialog.dart'; +import 'fake_ifile_open_dialog_factory.dart'; + +void main() { + final FakeIFileDialog fakeFileOpenDialog = FakeIFileDialog(); + final FakeIFileOpenDialogFactory fakeIFileOpenDialogFactory = + FakeIFileOpenDialogFactory(); + final FileDialogController fileDialogController = + FileDialogController(fakeFileOpenDialog, fakeIFileOpenDialogFactory); + + setUp(() { + fakeIFileOpenDialogFactory.mockSuccess(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.mockSuccess(); + }); + + tearDown(() { + fakeFileOpenDialog.resetCounters(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.resetCounters(); + }); + + test('setFolder should call dialog setFolder', () { + final Pointer ptrFolder = calloc(); + fileDialogController.setFolder(ptrFolder); + free(ptrFolder); + expect(fakeFileOpenDialog.setFolderCalledTimes(), 1); + }); + + test('setFileName should call dialog setFileName', () { + fileDialogController.setFileName('fileName'); + expect(fakeFileOpenDialog.setFileNameCalledTimes(), 1); + }); + + test('setFileTypes should call dialog setFileTypes', () { + final Pointer ptrFilters = calloc(); + fileDialogController.setFileTypes(1, ptrFilters); + free(ptrFilters); + expect(fakeFileOpenDialog.setFileTypesCalledTimes(), 1); + }); + + test('setOkButtonLabel should call dialog setOkButtonLabel', () { + fileDialogController.setOkButtonLabel('button'); + expect(fakeFileOpenDialog.setOkButtonLabelCalledTimes(), 1); + }); + + test('show should call dialog show', () { + fileDialogController.show(0); + expect(fakeFileOpenDialog.showCalledTimes(), 1); + }); + + test('getOptions should call dialog getOptions', () { + final Pointer ptrOptions = calloc(); + fileDialogController.getOptions(ptrOptions); + free(ptrOptions); + expect(fakeFileOpenDialog.getOptionsCalledTimes(), 1); + }); + + test('setOptions should call dialog setOptions', () { + fileDialogController.setOptions(32); + expect(fakeFileOpenDialog.setOptionsCalledTimes(), 1); + }); + + test('getResult should call dialog getResult', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResult(ptrCOMObject); + free(ptrCOMObject); + expect(fakeFileOpenDialog.getResultCalledTimes(), 1); + }); + + test('getResults should call the from method of the factory', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect(fakeIFileOpenDialogFactory.getFromCalledTimes(), 1); + }); + + test('getResults should call dialog getResults', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getResultsCalledTimes(), + 1); + }); + + test( + 'getResults should return an error when building a file open dialog throws', + () { + final Pointer> ptrCOMObject = + calloc>(); + fakeIFileOpenDialogFactory.mockFailure(); + free(ptrCOMObject); + expect(fileDialogController.getResults(ptrCOMObject), E_FAIL); + }); + + test( + 'getResults should return an error and release the dialog when getting results throws', + () { + final Pointer> ptrCOMObject = + calloc>(); + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.mockFailure(); + free(ptrCOMObject); + expect(fileDialogController.getResults(ptrCOMObject), E_FAIL); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getReleaseCalledTimes(), + 1); + }); + + test('getResults should call dialog release', () { + final Pointer> ptrCOMObject = + calloc>(); + fileDialogController.getResults(ptrCOMObject); + free(ptrCOMObject); + expect( + fakeIFileOpenDialogFactory.fakeIFileOpenDialog.getReleaseCalledTimes(), + 1); + }); +}