From b1797c2bb88680b517f29b1d19b644eb8ecd2baa Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 10 Jan 2023 12:11:18 -0800 Subject: [PATCH] [file_selector] Switch to Pigeon for macOS (#6902) * Initial pigeon definition * Update Dart, and add Dart test coverage * Update Swift and native tests * Version bump * Format * Revert SDK change --- .../file_selector_macos/CHANGELOG.md | 4 + .../macos/RunnerTests/RunnerTests.swift | 120 ++++--- .../lib/file_selector_macos.dart | 105 +++--- .../lib/src/messages.g.dart | 227 ++++++++++++ .../macos/Classes/FileSelectorPlugin.swift | 113 ++---- .../macos/Classes/messages.g.swift | 228 ++++++++++++ .../file_selector_macos/pigeons/copyright.txt | 3 + .../file_selector_macos/pigeons/messages.dart | 84 +++++ .../file_selector_macos/pubspec.yaml | 5 +- .../test/file_selector_macos_test.dart | 335 ++++++++++-------- .../test/file_selector_macos_test.mocks.dart | 51 +++ .../test/messages_test.g.dart | 107 ++++++ 12 files changed, 1042 insertions(+), 340 deletions(-) create mode 100644 packages/file_selector/file_selector_macos/lib/src/messages.g.dart create mode 100644 packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift create mode 100644 packages/file_selector/file_selector_macos/pigeons/copyright.txt create mode 100644 packages/file_selector/file_selector_macos/pigeons/messages.dart create mode 100644 packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart create mode 100644 packages/file_selector/file_selector_macos/test/messages_test.g.dart diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index af17db8ae3ef..27d08ae26771 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.0+4 + +* Converts platform channel to Pigeon. + ## 0.9.0+3 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift index bffc3452c49d..2dbd016f66ef 100644 --- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift +++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift @@ -47,9 +47,13 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -72,16 +76,16 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: [ - "initialDirectory": "/some/dir", - "suggestedName": "a name", - "confirmButtonText": "Open it!", - ] - ) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + directoryPath: "/some/dir", + nameFieldStringValue: "a name", + prompt: "Open it!")) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -104,12 +108,12 @@ class exampleTests: XCTestCase { panelController.openURLs = returnPaths.map({ path in URL(fileURLWithPath: path) }) let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: ["multiple": true] - ) - plugin.handle(call) { result in - let paths = (result as! [String]?)! + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in XCTAssertEqual(paths.count, returnPaths.count) XCTAssertEqual(paths[0], returnPaths[0]) XCTAssertEqual(paths[1], returnPaths[1]) @@ -130,17 +134,17 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "openFile", - arguments: [ - "acceptedTypes": [ - "extensions": ["txt", "json"], - "UTIs": ["public.text", "public.image"], - ] - ] - ) - plugin.handle(call) { result in - XCTAssertEqual((result as! [String]?)![0], returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: AllowedTypes( + extensions: ["txt", "json"], + mimeTypes: [], + utis: ["public.text", "public.image"]))) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -158,9 +162,13 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "openFile", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths.count, 0) called.fulfill() } @@ -178,9 +186,9 @@ class exampleTests: XCTestCase { panelController.saveURL = URL(fileURLWithPath: returnPath) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = SavePanelOptions() + plugin.displaySavePanel(options: options) { path in + XCTAssertEqual(path, returnPath) called.fulfill() } @@ -198,15 +206,11 @@ class exampleTests: XCTestCase { panelController.saveURL = URL(fileURLWithPath: returnPath) let called = XCTestExpectation() - let call = FlutterMethodCall( - methodName: "getSavePath", - arguments: [ - "initialDirectory": "/some/dir", - "confirmButtonText": "Save it!", - ] - ) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = SavePanelOptions( + directoryPath: "/some/dir", + prompt: "Save it!") + plugin.displaySavePanel(options: options) { path in + XCTAssertEqual(path, returnPath) called.fulfill() } @@ -225,9 +229,9 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getSavePath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = SavePanelOptions() + plugin.displaySavePanel(options: options) { path in + XCTAssertNil(path) called.fulfill() } @@ -245,9 +249,13 @@ class exampleTests: XCTestCase { panelController.openURLs = [URL(fileURLWithPath: returnPath)] let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertEqual(result as! String?, returnPath) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths[0], returnPath) called.fulfill() } @@ -270,9 +278,13 @@ class exampleTests: XCTestCase { panelController: panelController) let called = XCTestExpectation() - let call = FlutterMethodCall(methodName: "getDirectoryPath", arguments: [:]) - plugin.handle(call) { result in - XCTAssertNil(result) + let options = OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions()) + plugin.displayOpenPanel(options: options) { paths in + XCTAssertEqual(paths.count, 0) called.fulfill() } diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart index 74ce2835d18c..f8a087fa6877 100644 --- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart +++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart @@ -3,17 +3,12 @@ // found in the LICENSE file. import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/file_selector_macos'); +import 'src/messages.g.dart'; /// An implementation of [FileSelectorPlatform] for macOS. class FileSelectorMacOS extends FileSelectorPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + final FileSelectorApi _hostApi = FileSelectorApi(); /// Registers the macOS implementation. static void registerWith() { @@ -26,16 +21,17 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? path = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': false, - }, - ); - return path == null ? null : XFile(path.first); + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? null : XFile(paths.first!); } @override @@ -44,16 +40,17 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - final List? pathList = await _channel.invokeListMethod( - 'openFile', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - 'multiple': true, - }, - ); - return pathList?.map((String path) => XFile(path)).toList() ?? []; + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: true, + canChooseDirectories: false, + canChooseFiles: true, + baseOptions: SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.map((String? path) => XFile(path!)).toList(); } @override @@ -63,15 +60,12 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? suggestedName, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getSavePath', - { - 'acceptedTypes': _allowedTypeListFromTypeGroups(acceptedTypeGroups), - 'initialDirectory': initialDirectory, - 'suggestedName': suggestedName, - 'confirmButtonText': confirmButtonText, - }, - ); + return _hostApi.displaySavePanel(SavePanelOptions( + allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups), + directoryPath: initialDirectory, + nameFieldStringValue: suggestedName, + prompt: confirmButtonText, + )); } @override @@ -79,30 +73,29 @@ class FileSelectorMacOS extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( - 'getDirectoryPath', - { - 'initialDirectory': initialDirectory, - 'confirmButtonText': confirmButtonText, - }, - ); + final List paths = + await _hostApi.displayOpenPanel(OpenPanelOptions( + allowsMultipleSelection: false, + canChooseDirectories: true, + canChooseFiles: false, + baseOptions: SavePanelOptions( + directoryPath: initialDirectory, + prompt: confirmButtonText, + ))); + return paths.isEmpty ? null : paths.first; } // Converts the type group list into a flat list of all allowed types, since // macOS doesn't support filter groups. - Map>? _allowedTypeListFromTypeGroups( - List? typeGroups) { - const String extensionKey = 'extensions'; - const String mimeTypeKey = 'mimeTypes'; - const String utiKey = 'UTIs'; + AllowedTypes? _allowedTypesFromTypeGroups(List? typeGroups) { if (typeGroups == null || typeGroups.isEmpty) { return null; } - final Map> allowedTypes = >{ - extensionKey: [], - mimeTypeKey: [], - utiKey: [], - }; + final AllowedTypes allowedTypes = AllowedTypes( + extensions: [], + mimeTypes: [], + utis: [], + ); for (final XTypeGroup typeGroup in typeGroups) { // If any group allows everything, no filtering should be done. if (typeGroup.allowsAny) { @@ -119,9 +112,9 @@ class FileSelectorMacOS extends FileSelectorPlatform { '"mimeTypes" must be non-empty for macOS if anything is ' 'non-empty.'); } - allowedTypes[extensionKey]!.addAll(typeGroup.extensions ?? []); - allowedTypes[mimeTypeKey]!.addAll(typeGroup.mimeTypes ?? []); - allowedTypes[utiKey]!.addAll(typeGroup.macUTIs ?? []); + allowedTypes.extensions.addAll(typeGroup.extensions ?? []); + allowedTypes.mimeTypes.addAll(typeGroup.mimeTypes ?? []); + allowedTypes.utis.addAll(typeGroup.macUTIs ?? []); } return allowedTypes; diff --git a/packages/file_selector/file_selector_macos/lib/src/messages.g.dart b/packages/file_selector/file_selector_macos/lib/src/messages.g.dart new file mode 100644 index 000000000000..5f1daf94283e --- /dev/null +++ b/packages/file_selector/file_selector_macos/lib/src/messages.g.dart @@ -0,0 +1,227 @@ +// 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. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +class AllowedTypes { + AllowedTypes({ + required this.extensions, + required this.mimeTypes, + required this.utis, + }); + + List extensions; + + List mimeTypes; + + List utis; + + Object encode() { + return [ + extensions, + mimeTypes, + utis, + ]; + } + + static AllowedTypes decode(Object result) { + result as List; + return AllowedTypes( + extensions: (result[0] as List?)!.cast(), + mimeTypes: (result[1] as List?)!.cast(), + utis: (result[2] as List?)!.cast(), + ); + } +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +class SavePanelOptions { + SavePanelOptions({ + this.allowedFileTypes, + this.directoryPath, + this.nameFieldStringValue, + this.prompt, + }); + + AllowedTypes? allowedFileTypes; + + String? directoryPath; + + String? nameFieldStringValue; + + String? prompt; + + Object encode() { + return [ + allowedFileTypes?.encode(), + directoryPath, + nameFieldStringValue, + prompt, + ]; + } + + static SavePanelOptions decode(Object result) { + result as List; + return SavePanelOptions( + allowedFileTypes: result[0] != null + ? AllowedTypes.decode(result[0]! as List) + : null, + directoryPath: result[1] as String?, + nameFieldStringValue: result[2] as String?, + prompt: result[3] as String?, + ); + } +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +class OpenPanelOptions { + OpenPanelOptions({ + required this.allowsMultipleSelection, + required this.canChooseDirectories, + required this.canChooseFiles, + required this.baseOptions, + }); + + bool allowsMultipleSelection; + + bool canChooseDirectories; + + bool canChooseFiles; + + SavePanelOptions baseOptions; + + Object encode() { + return [ + allowsMultipleSelection, + canChooseDirectories, + canChooseFiles, + baseOptions.encode(), + ]; + } + + static OpenPanelOptions decode(Object result) { + result as List; + return OpenPanelOptions( + allowsMultipleSelection: result[0]! as bool, + canChooseDirectories: result[1]! as bool, + canChooseFiles: result[2]! as bool, + baseOptions: SavePanelOptions.decode(result[3]! as List), + ); + } +} + +class _FileSelectorApiCodec extends StandardMessageCodec { + const _FileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AllowedTypes) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is OpenPanelOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SavePanelOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AllowedTypes.decode(readValue(buffer)!); + + case 129: + return OpenPanelOptions.decode(readValue(buffer)!); + + case 130: + return SavePanelOptions.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FileSelectorApi { + /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FileSelectorApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FileSelectorApiCodec(); + + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + Future> displayOpenPanel(OpenPanelOptions arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displayOpenPanel', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_options]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + Future displaySavePanel(SavePanelOptions arg_options) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displaySavePanel', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_options]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return (replyList[0] as String?); + } + } +} diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift index 9551671d1575..4e1c935dad73 100644 --- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift @@ -40,7 +40,7 @@ protocol ViewProvider { var view: NSView? { get } } -public class FileSelectorPlugin: NSObject, FlutterPlugin { +public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { private let viewProvider: ViewProvider private let panelController: PanelController @@ -49,13 +49,10 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { private let saveMethod = "getSavePath" public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/file_selector_macos", - binaryMessenger: registrar.messenger) let instance = FileSelectorPlugin( viewProvider: DefaultViewProvider(registrar: registrar), panelController: DefaultPanelController()) - registrar.addMethodCallDelegate(instance, channel: channel) + FileSelectorApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance) } init(viewProvider: ViewProvider, panelController: PanelController) { @@ -63,30 +60,20 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { self.panelController = panelController } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - let arguments = (call.arguments ?? [:]) as! [String: Any] - switch call.method { - case openMethod, - openDirectoryMethod: - let choosingDirectory = call.method == openDirectoryMethod - let panel = NSOpenPanel() - configure(panel: panel, with: arguments) - configure(openPanel: panel, with: arguments, choosingDirectory: choosingDirectory) - panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in - if (choosingDirectory) { - result(selection?.first?.path) - } else { - result(selection?.map({ item in item.path })) - } - } - case saveMethod: - let panel = NSSavePanel() - configure(panel: panel, with: arguments) - panelController.display(panel, for: viewProvider.view?.window) { (selection: URL?) in - result(selection?.path) - } - default: - result(FlutterMethodNotImplemented) + func displayOpenPanel(options: OpenPanelOptions, completion: @escaping ([String?]) -> Void) { + + let panel = NSOpenPanel() + configure(openPanel: panel, with: options) + panelController.display(panel, for: viewProvider.view?.window) { (selection: [URL]?) in + completion(selection?.map({ item in item.path }) ?? []) + } + } + + func displaySavePanel(options: SavePanelOptions, completion: @escaping (String?) -> Void) { + let panel = NSSavePanel() + configure(panel: panel, with: options) + panelController.display(panel, for: viewProvider.view?.window) { (selection: URL?) in + completion(selection?.path) } } @@ -94,28 +81,25 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { /// - Parameters: /// - panel: The panel to configure. /// - arguments: The arguments dictionary from a FlutterMethodCall to this plugin. - private func configure(panel: NSSavePanel, with arguments: [String: Any]) { - if let initialDirectory = getNonNullStringValue(for: "initialDirectory", from: arguments) { - panel.directoryURL = URL(fileURLWithPath: initialDirectory) + private func configure(panel: NSSavePanel, with options: SavePanelOptions) { + if let directoryPath = options.directoryPath { + panel.directoryURL = URL(fileURLWithPath: directoryPath) } - if let suggestedName = getNonNullStringValue(for: "suggestedName", from: arguments) { + if let suggestedName = options.nameFieldStringValue { panel.nameFieldStringValue = suggestedName } - if let confirmButtonText = getNonNullStringValue(for: "confirmButtonText", from: arguments) { - panel.prompt = confirmButtonText + if let prompt = options.prompt { + panel.prompt = prompt } - let acceptedTypes = getNonNullValue( - for: "acceptedTypes", - from: arguments - ) as! [String: Any]? - if let acceptedTypes = acceptedTypes { + if let acceptedTypes = options.allowedFileTypes { var allowedTypes: [String] = [] - let extensions = getNonNullStringArrayValue(for: "extensions", from: acceptedTypes) - let UTIs = getNonNullStringArrayValue(for: "UTIs", from: acceptedTypes) - allowedTypes.append(contentsOf: extensions) - allowedTypes.append(contentsOf: UTIs) - // TODO: Add support for mimeTypes in macOS 11+. + // The array values are non-null by convention even though Pigeon can't currently express + // that via the types; see messages.dart. + allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! })) + allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! })) + // TODO: Add support for mimeTypes in macOS 11+. See + // https://github.com/flutter/flutter/issues/117843 if !allowedTypes.isEmpty { panel.allowedFileTypes = allowedTypes @@ -130,13 +114,12 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin { /// - choosingDirectory: True if the panel should allow choosing directories rather than files. private func configure( openPanel panel: NSOpenPanel, - with arguments: [String: Any], - choosingDirectory: Bool + with options: OpenPanelOptions ) { - panel.allowsMultipleSelection = - getNonNullValue(for: "multiple", from: arguments) as! Bool? ?? false - panel.canChooseDirectories = choosingDirectory; - panel.canChooseFiles = !choosingDirectory; + configure(panel: panel, with: options.baseOptions) + panel.allowsMultipleSelection = options.allowsMultipleSelection + panel.canChooseDirectories = options.canChooseDirectories; + panel.canChooseFiles = options.canChooseFiles; } } @@ -188,31 +171,3 @@ private class DefaultViewProvider: ViewProvider { } } } - -/// Returns the value for the given key from the provided dictionary, unless the value is NSNull -/// in which case it returns nil. -/// - Parameters: -/// - key: The key to get a value for. -/// - dictionary: The dictionary to get the value from. -/// - Returns: The value, or nil for NSNull. -private func getNonNullValue(for key: String, from dictionary: [String: Any]) -> Any? { - let value = dictionary[key]; - return value is NSNull ? nil : value; -} - -/// A convenience wrapper for getNonNullValue for string values. -private func getNonNullStringValue(for key: String, from dictionary: [String: Any]) -> String? { - return getNonNullValue(for: key, from: dictionary) as! String? -} - -/// A convenience wrapper for getNonNullValue for array-of-string values. -/// - Parameters: -/// - key: The key to get a value for. -/// - dictionary: The dictionary to get the value from. -/// - Returns: The value, or an empty array for nil for NSNull. -private func getNonNullStringArrayValue( - for key: String, - from dictionary: [String: Any] -) -> [String] { - return getNonNullValue(for: key, from: dictionary) as! [String]? ?? [] -} diff --git a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift new file mode 100644 index 000000000000..75753d962525 --- /dev/null +++ b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift @@ -0,0 +1,228 @@ +// 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. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + + +/// Generated class from Pigeon. + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct AllowedTypes { + var extensions: [String?] + var mimeTypes: [String?] + var utis: [String?] + + static func fromList(_ list: [Any?]) -> AllowedTypes? { + let extensions = list[0] as! [String?] + let mimeTypes = list[1] as! [String?] + let utis = list[2] as! [String?] + + return AllowedTypes( + extensions: extensions, + mimeTypes: mimeTypes, + utis: utis + ) + } + func toList() -> [Any?] { + return [ + extensions, + mimeTypes, + utis, + ] + } +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +/// +/// Generated class from Pigeon that represents data sent in messages. +struct SavePanelOptions { + var allowedFileTypes: AllowedTypes? = nil + var directoryPath: String? = nil + var nameFieldStringValue: String? = nil + var prompt: String? = nil + + static func fromList(_ list: [Any?]) -> SavePanelOptions? { + var allowedFileTypes: AllowedTypes? = nil + if let allowedFileTypesList = list[0] as? [Any?] { + allowedFileTypes = AllowedTypes.fromList(allowedFileTypesList) + } + let directoryPath = list[1] as? String + let nameFieldStringValue = list[2] as? String + let prompt = list[3] as? String + + return SavePanelOptions( + allowedFileTypes: allowedFileTypes, + directoryPath: directoryPath, + nameFieldStringValue: nameFieldStringValue, + prompt: prompt + ) + } + func toList() -> [Any?] { + return [ + allowedFileTypes?.toList(), + directoryPath, + nameFieldStringValue, + prompt, + ] + } +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct OpenPanelOptions { + var allowsMultipleSelection: Bool + var canChooseDirectories: Bool + var canChooseFiles: Bool + var baseOptions: SavePanelOptions + + static func fromList(_ list: [Any?]) -> OpenPanelOptions? { + let allowsMultipleSelection = list[0] as! Bool + let canChooseDirectories = list[1] as! Bool + let canChooseFiles = list[2] as! Bool + let baseOptions = SavePanelOptions.fromList(list[3] as! [Any?])! + + return OpenPanelOptions( + allowsMultipleSelection: allowsMultipleSelection, + canChooseDirectories: canChooseDirectories, + canChooseFiles: canChooseFiles, + baseOptions: baseOptions + ) + } + func toList() -> [Any?] { + return [ + allowsMultipleSelection, + canChooseDirectories, + canChooseFiles, + baseOptions.toList(), + ] + } +} + +private class FileSelectorApiCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return AllowedTypes.fromList(self.readValue() as! [Any]) + case 129: + return OpenPanelOptions.fromList(self.readValue() as! [Any]) + case 130: + return SavePanelOptions.fromList(self.readValue() as! [Any]) + default: + return super.readValue(ofType: type) + + } + } +} +private class FileSelectorApiCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? AllowedTypes { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? OpenPanelOptions { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? SavePanelOptions { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class FileSelectorApiCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return FileSelectorApiCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return FileSelectorApiCodecWriter(data: data) + } +} + +class FileSelectorApiCodec: FlutterStandardMessageCodec { + static let shared = FileSelectorApiCodec(readerWriter: FileSelectorApiCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol FileSelectorApi { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + func displayOpenPanel(options: OpenPanelOptions, completion: @escaping ([String?]) -> Void) + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + func displaySavePanel(options: SavePanelOptions, completion: @escaping (String?) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class FileSelectorApiSetup { + /// The codec used by FileSelectorApi. + static var codec: FlutterStandardMessageCodec { FileSelectorApiCodec.shared } + /// Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: FileSelectorApi?) { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + displayOpenPanelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! OpenPanelOptions + api.displayOpenPanel(options: optionsArg) { result in + reply(wrapResult(result)) + } + } + } else { + displayOpenPanelChannel.setMessageHandler(nil) + } + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + displaySavePanelChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let optionsArg = args[0] as! SavePanelOptions + api.displaySavePanel(options: optionsArg) { result in + reply(wrapResult(result)) + } + } + } else { + displaySavePanelChannel.setMessageHandler(nil) + } + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: FlutterError) -> [Any?] { + return [ + error.code, + error.message, + error.details + ] +} diff --git a/packages/file_selector/file_selector_macos/pigeons/copyright.txt b/packages/file_selector/file_selector_macos/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/file_selector/file_selector_macos/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/file_selector/file_selector_macos/pigeons/messages.dart b/packages/file_selector/file_selector_macos/pigeons/messages.dart new file mode 100644 index 000000000000..85b2996baf8a --- /dev/null +++ b/packages/file_selector/file_selector_macos/pigeons/messages.dart @@ -0,0 +1,84 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + input: 'pigeons/messages.dart', + swiftOut: 'macos/Classes/messages.g.swift', + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/messages_test.g.dart', + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// A Pigeon representation of the macOS portion of an `XTypeGroup`. +class AllowedTypes { + const AllowedTypes({ + this.extensions = const [], + this.mimeTypes = const [], + this.utis = const [], + }); + + // TODO(stuartmorgan): Declare these as non-nullable generics once + // https://github.com/flutter/flutter/issues/97848 is fixed. In practice, + // the values will never be null, and the native implementation assumes that. + final List extensions; + final List mimeTypes; + final List utis; +} + +/// Options for save panels. +/// +/// These correspond to NSSavePanel properties (which are, by extension +/// NSOpenPanel properties as well). +class SavePanelOptions { + const SavePanelOptions({ + this.allowedFileTypes, + this.directoryPath, + this.nameFieldStringValue, + this.prompt, + }); + final AllowedTypes? allowedFileTypes; + final String? directoryPath; + final String? nameFieldStringValue; + final String? prompt; +} + +/// Options for open panels. +/// +/// These correspond to NSOpenPanel properties. +class OpenPanelOptions extends SavePanelOptions { + const OpenPanelOptions({ + this.allowsMultipleSelection = false, + this.canChooseDirectories = false, + this.canChooseFiles = true, + this.baseOptions = const SavePanelOptions(), + }); + final bool allowsMultipleSelection; + final bool canChooseDirectories; + final bool canChooseFiles; + // NSOpenPanel inherits from NSSavePanel, so shares all of its options. + // Ideally this would be done with inheritance rather than composition, but + // Pigeon doesn't currently support data class inheritance: + // https://github.com/flutter/flutter/issues/117819. + final SavePanelOptions baseOptions; +} + +@HostApi(dartHostTestHandler: 'TestFileSelectorApi') +abstract class FileSelectorApi { + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + // TODO(stuartmorgan): Declare this return as a non-nullable generic once + // https://github.com/flutter/flutter/issues/97848 is fixed. In practice, + // the values will never be null, and the calling code assumes that. + @async + List displayOpenPanel(OpenPanelOptions options); + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + @async + String? displaySavePanel(SavePanelOptions options); +} diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index 3fc3832d7280..ac41a2525f4d 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_macos description: macOS implementation of the file_selector plugin. repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+3 +version: 0.9.0+4 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,5 +23,8 @@ dependencies: sdk: flutter dev_dependencies: + build_runner: ^2.3.2 flutter_test: sdk: flutter + mockito: ^5.3.2 + pigeon: ^4.2.14 diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart index 789d70a51777..181409e6f1b4 100644 --- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart @@ -3,24 +3,32 @@ // found in the LICENSE file. import 'package:file_selector_macos/file_selector_macos.dart'; +import 'package:file_selector_macos/src/messages.g.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'file_selector_macos_test.mocks.dart'; +import 'messages_test.g.dart'; + +@GenerateMocks([TestFileSelectorApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final FileSelectorMacOS plugin = FileSelectorMacOS(); - - final List log = []; + late FileSelectorMacOS plugin; + late MockTestFileSelectorApi mockApi; setUp(() { - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); - - log.clear(); + plugin = FileSelectorMacOS(); + mockApi = MockTestFileSelectorApi(); + TestFileSelectorApi.setup(mockApi); + + // Set default stubs for tests that don't expect a specific return value, + // so calls don't throw. Tests that `expect` return values should override + // these locally. + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null); }); test('registered instance', () { @@ -29,6 +37,33 @@ void main() { }); group('openFile', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo']); + + final XFile? file = await plugin.openFile(); + + expect(file!.path, 'foo'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, false); + expect(options.canChooseFiles, true); + expect(options.canChooseDirectories, false); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final XFile? file = await plugin.openFile(); + + expect(file, null); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -46,53 +81,33 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': { - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.allowedFileTypes!.extensions, + ['txt', 'jpg']); + expect(options.baseOptions.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.baseOptions.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -117,6 +132,34 @@ void main() { }); group('openFiles', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo', 'bar']); + + final List files = await plugin.openFiles(); + + expect(files[0].path, 'foo'); + expect(files[1].path, 'bar'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, true); + expect(options.canChooseFiles, true); + expect(options.canChooseDirectories, false); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final List files = await plugin.openFiles(); + + expect(files, isEmpty); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -134,53 +177,33 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.allowedFileTypes!.extensions, + ['txt', 'jpg']); + expect(options.baseOptions.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.baseOptions.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -205,6 +228,29 @@ void main() { }); group('getSavePath', () { + test('works as expected with no arguments', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo'); + + final String? path = await plugin.getSavePath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); + expect(options.directoryPath, null); + expect(options.nameFieldStringValue, null); + expect(options.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null); + + final String? path = await plugin.getSavePath(); + + expect(path, null); + }); + test('passes the accepted type groups correctly', () async { const XTypeGroup group = XTypeGroup( label: 'text', @@ -223,53 +269,32 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': >{ - 'extensions': ['txt', 'jpg'], - 'mimeTypes': ['text/plain', 'image/jpg'], - 'UTIs': ['public.text', 'public.image'], - }, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes!.extensions, ['txt', 'jpg']); + expect(options.allowedFileTypes!.mimeTypes, + ['text/plain', 'image/jpg']); + expect(options.allowedFileTypes!.utis, + ['public.text', 'public.image']); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.prompt, 'Open File'); }); test('throws for a type group that does not support macOS', () async { @@ -295,32 +320,49 @@ void main() { }); group('getDirectoryPath', () { + test('works as expected with no arguments', () async { + when(mockApi.displayOpenPanel(any)) + .thenAnswer((_) async => ['foo']); + + final String? path = await plugin.getDirectoryPath(); + + expect(path, 'foo'); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.allowsMultipleSelection, false); + expect(options.canChooseFiles, false); + expect(options.canChooseDirectories, true); + expect(options.baseOptions.allowedFileTypes, null); + expect(options.baseOptions.directoryPath, null); + expect(options.baseOptions.nameFieldStringValue, null); + expect(options.baseOptions.prompt, null); + }); + + test('handles cancel', () async { + when(mockApi.displayOpenPanel(any)).thenAnswer((_) async => []); + + final String? path = await plugin.getDirectoryPath(); + + expect(path, null); + }); + test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.directoryPath, '/example/directory'); }); test('passes confirmButtonText correctly', () async { await plugin.getDirectoryPath(confirmButtonText: 'Open File'); - expect( - log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], - ); + final VerificationResult result = + verify(mockApi.displayOpenPanel(captureAny)); + final OpenPanelOptions options = result.captured[0] as OpenPanelOptions; + expect(options.baseOptions.prompt, 'Open File'); }); }); @@ -343,16 +385,9 @@ void main() { ), ]); - expect( - log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypes': null, - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], - ); + final VerificationResult result = + verify(mockApi.displaySavePanel(captureAny)); + final SavePanelOptions options = result.captured[0] as SavePanelOptions; + expect(options.allowedFileTypes, null); }); } diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart new file mode 100644 index 000000000000..ddd563b2869a --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.mocks.dart @@ -0,0 +1,51 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in file_selector_macos/example/macos/Flutter/ephemeral/.symlinks/plugins/file_selector_macos/test/file_selector_macos_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_macos/src/messages.g.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'messages_test.g.dart' as _i2; + +// 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 [TestFileSelectorApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestFileSelectorApi extends _i1.Mock + implements _i2.TestFileSelectorApi { + MockTestFileSelectorApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future> displayOpenPanel(_i4.OpenPanelOptions? options) => + (super.noSuchMethod( + Invocation.method( + #displayOpenPanel, + [options], + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); + @override + _i3.Future displaySavePanel(_i4.SavePanelOptions? options) => + (super.noSuchMethod( + Invocation.method( + #displaySavePanel, + [options], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/packages/file_selector/file_selector_macos/test/messages_test.g.dart b/packages/file_selector/file_selector_macos/test/messages_test.g.dart new file mode 100644 index 000000000000..731f1fb1d51f --- /dev/null +++ b/packages/file_selector/file_selector_macos/test/messages_test.g.dart @@ -0,0 +1,107 @@ +// 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. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:file_selector_macos/src/messages.g.dart'; + +class _TestFileSelectorApiCodec extends StandardMessageCodec { + const _TestFileSelectorApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is AllowedTypes) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is OpenPanelOptions) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SavePanelOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return AllowedTypes.decode(readValue(buffer)!); + + case 129: + return OpenPanelOptions.decode(readValue(buffer)!); + + case 130: + return SavePanelOptions.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestFileSelectorApi { + static const MessageCodec codec = _TestFileSelectorApiCodec(); + + /// Shows an open panel with the given [options], returning the list of + /// selected paths. + /// + /// An empty list corresponds to a cancelled selection. + Future> displayOpenPanel(OpenPanelOptions options); + + /// Shows a save panel with the given [options], returning the selected path. + /// + /// A null return corresponds to a cancelled save. + Future displaySavePanel(SavePanelOptions options); + + static void setup(TestFileSelectorApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displayOpenPanel', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displayOpenPanel was null.'); + final List args = (message as List?)!; + final OpenPanelOptions? arg_options = (args[0] as OpenPanelOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displayOpenPanel was null, expected non-null OpenPanelOptions.'); + final List output = await api.displayOpenPanel(arg_options!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileSelectorApi.displaySavePanel', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displaySavePanel was null.'); + final List args = (message as List?)!; + final SavePanelOptions? arg_options = (args[0] as SavePanelOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.FileSelectorApi.displaySavePanel was null, expected non-null SavePanelOptions.'); + final String? output = await api.displaySavePanel(arg_options!); + return [output]; + }); + } + } + } +}