From f9ee25f4a9f2535befeefdcbfee237160c3d8f23 Mon Sep 17 00:00:00 2001 From: eugerossetto Date: Wed, 28 Sep 2022 11:56:25 -0300 Subject: [PATCH] Add win32 package and base dialog wrapper class. Add methods of FileDialogController Add FileDialogController tests. Change the location where the free of the pointer is placed. --- .../file_dialog_controller.dart | 91 ++++++++++++ .../ifile_open_dialog_factory.dart | 13 ++ .../file_selector_windows/pubspec.yaml | 2 + .../file_selector_dart/fake_file_dialog.dart | 112 +++++++++++++++ .../fake_ifile_open_dialog.dart | 55 ++++++++ .../fake_ifile_open_dialog_factory.dart | 40 ++++++ .../file_dialog_controller_test.dart | 131 ++++++++++++++++++ 7 files changed, 444 insertions(+) create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/file_dialog_controller.dart create mode 100644 packages/file_selector/file_selector_windows/lib/src/file_selector_dart/ifile_open_dialog_factory.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_file_dialog.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/fake_ifile_open_dialog_factory.dart create mode 100644 packages/file_selector/file_selector_windows/test/file_selector_dart/file_dialog_controller_test.dart 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/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/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_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_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); + }); +}