Skip to content

Commit cbd9bf6

Browse files
committed
feat: add instanceOrNull and instanceOrThrow static getters to NativeImagePickerMacOS, add example test, testing section in README
1 parent 7ad8ec7 commit cbd9bf6

8 files changed

+812
-10
lines changed

Diff for: README.md

+119-2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ final bool isRegistered = ImagePickerPlatform.instance is NativeImagePickerMacOS
8686
**To checks if the current macOS version supports this implementation**:
8787

8888
```dart
89+
<!-- TODO: Make this instance method? -->
8990
final bool isSupported = await NativeImagePickerMacOS().isSupported(); // Returns false on non-macOS platforms or if PHPicker is not supported on the current macOS version.
9091
```
9192

@@ -110,8 +111,7 @@ NativeImagePickerMacOS.registerWith();
110111
**To open the macOS photos app**:
111112

112113
```dart
113-
<!-- TODO: This approach needs more consideration, also update the example (which has related TODO) -->
114-
await NativeImagePickerMacOS().openPhotosApp();
114+
await NativeImagePickerMacOS.instanceOrNull?.openPhotosApp();
115115
116116
// OR
117117
@@ -124,6 +124,8 @@ if (imagePickerImplementation is NativeImagePickerMacOS) {
124124
> [!TIP]
125125
> You can use `NativeImagePickerMacOS.registerWith()` to register this implementation. However, this bypasses platform checks, which **may result in runtime errors** if the current platform is not macOS or if the macOS version is unsupported. Instead, use `registerWithIfSupported()` if uncertain.
126126
127+
Refer to the [example main.dart](example/lib/main.dart) for a full usage example.
128+
127129
## 🌱 Contributing
128130

129131
This package uses [pigeon](https://pub.dev/packages/pigeon) for platform communication with the platform host and [mockito](https://pub.dev/packages/mockito) for mocking in unit tests and [swift-format](https://github.com/swiftlang/swift-format) for formatting the Swift code.
@@ -162,3 +164,118 @@ This functionality was originally proposed as a [pull request to `image_picker_m
162164
<img src="https://github.com/CompileKernel/native-image-picker-macos/blob/main/readme_assets/phpicker-window.png?raw=true" alt="PHPicker window" width="600">
163165

164166
[Ask a question about using the package](https://github.com/CompileKernel/native-image-picker-macos/discussions/new?category=q-a).
167+
168+
## 🧪 Testing
169+
170+
> [!TIP]
171+
> With this approach, you can effectively test this platform implementation with the existing packages that use `image_picker` APIs. All platform-specific calls to `NativeImagePickerMacOS` should use the instance from `ImagePickerPlatform.interface` instead of creating a new `NativeImagePickerMacOS` to work.
172+
173+
To override the methods implementation for unit testing, add the dev dependencies:
174+
175+
1. [`mockito`](https://pub.dev/packages/mockito) (or [`mocktail`](https://pub.dev/packages/mocktail)): for mocking the instance methods of `NativeImagePickerMacOS`.
176+
2. [`image_picker_platform_interface`](https://pub.dev/packages/image_picker_platform_interface): for overriding the instance of `ImagePickerPlatform` with the mock instance.
177+
3. [`build_runner`](https://pub.dev/packages/build_runner): for creating the generated Dart files.
178+
4. [`plugin_platform_interface`](https://pub.dev/packages/plugin_platform_interface): Since `ImagePickerPlatform` extends `PlatformInterface`, it's required to apply the mixin `MockPlatformInterfaceMixin` to the mock of `NativeImagePickerMacOS` to [ignore an assertation failure that enforces the usage of `extends` instead of `implements`](https://pub.dev/packages/plugin_platform_interface), since mock classes need to extend `Mock` and implement the real class.
179+
180+
```shell
181+
$ flutter pub add dev:mockito dev:image_picker_platform_interface dev:build_runner dev:plugin_platform_interface # Add them as dev-dependencies
182+
```
183+
184+
In your test file, add this annotation somewhere:
185+
186+
```dart
187+
import 'package:mockito/annotations.dart';
188+
import 'package:mockito/mockito.dart';
189+
import 'package:native_image_picker_macos/native_image_picker_macos.dart';
190+
191+
@GenerateNiceMocks([MockSpec<NativeImagePickerMacOS>()])
192+
```
193+
194+
Generate the `MockNativeImagePickerMacOS` by running:
195+
196+
```shell
197+
$ dart run build_runner build --delete-conflicting-outputs
198+
```
199+
200+
Create a new instance of `MockNativeImagePickerMacOS` and override the instance of `ImagePickerPlatform` to every test:
201+
202+
```dart
203+
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
204+
import 'package:flutter_test/flutter_test.dart';
205+
206+
void main() {
207+
TestWidgetsFlutterBinding.ensureInitialized();
208+
209+
late MockNativeImagePickerMacOS mockNativeImagePickerMacOS;
210+
211+
setUp(() {
212+
mockNativeImagePickerMacOS = MockNativeImagePickerMacOS();
213+
214+
ImagePickerPlatform.instance = mockNativeImagePickerMacOS;
215+
});
216+
217+
// Your tests, example:
218+
219+
testWidgets(
220+
'pressing the open photos button calls openPhotosApp from $NativeImagePickerMacOS',
221+
(WidgetTester tester) async {
222+
await tester
223+
.pumpWidget(const ExampleWidget()); // REPLACE WITH THE TARGET WIDGET
224+
225+
final openPhotosFinder =
226+
find.text('Open Photos App'); // REPLACE WITH THE BUTTON TEXT
227+
228+
expect(openPhotosFinder, findsOneWidget);
229+
230+
// Assuming the openPhotosApp call will success.
231+
when(mockNativeImagePickerMacOS.openPhotosApp())
232+
.thenAnswer((_) async => true);
233+
234+
await tester.tap(openPhotosFinder);
235+
await tester.pump();
236+
237+
verify(mockNativeImagePickerMacOS.openPhotosApp()).called(1);
238+
verifyNoMoreInteractions(mockNativeImagePickerMacOS);
239+
},
240+
);
241+
242+
// ...
243+
}
244+
```
245+
246+
However, if you run the tests, you will get the following error:
247+
248+
```console
249+
Assertion failed: "Platform interfaces must not be implemented with `implements`"
250+
```
251+
252+
And that is because by default, all plugin platform interfaces that inherit from `PlatformInterface` must `extends` and not `implements` it to avoid breaking changes (adding new methods to platform interfaces are not considered breaking changes).
253+
254+
And mock classes must `implements` the real class rather than `extends` them, a solution is to [provide the mixin `MockPlatformInterfaceMixin` from `plugin_platform_interface` that will override this check](https://pub.dev/packages/plugin_platform_interface#mocking-or-faking-platform-interfaces):
255+
256+
```dart
257+
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
258+
259+
// This doesn't work yet since MockNativeImagePickerMacOS is generated, unlike the mocktail package.
260+
class MockNativeImagePickerMacOS extends Mock
261+
with MockPlatformInterfaceMixin
262+
implements NativeImagePickerMacOS {}
263+
```
264+
265+
And since `MockNativeImagePickerMacOS` is generated, we need a new class that extends the base mock and provides the `MockPlatformInterfaceMixin` for `plugin_platform_interface` to not throw the assertion failure:
266+
267+
```dart
268+
@GenerateNiceMocks([MockSpec<NativeImagePickerMacOS>(as: Symbol('BaseMockNativeImagePickerMacOS'))]) // This name should be different than MockNativeImagePickerMacOS for the mockito generation to success
269+
import '<current-test-file-name>.mocks.dart'; // REPLACE <current-test-file-name> with the current test file name without extension
270+
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
271+
272+
class MockNativeImagePickerMacOS extends BaseMockNativeImagePickerMacOS
273+
with MockPlatformInterfaceMixin {}
274+
275+
// Use MockNativeImagePickerMacOS instead of BaseMockNativeImagePickerMacOS for creating the mock of NativeImagePickerMacOS
276+
```
277+
278+
Refer to the [example main_test.dart](example/test/main_test.dart) for the full example test.
279+
280+
> [!NOTE]
281+
> Refer to the [Flutter documentation on mocking dependencies using Mockito](https://docs.flutter.dev/cookbook/testing/unit/mocking) for more details.

Diff for: example/lib/main.dart

+9-6
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ class _ButtonsState extends State<Buttons> {
7676
child: Column(
7777
spacing: 8,
7878
children: [
79-
ElevatedButton.icon(
80-
label: Text('Open Photos App'),
81-
icon: Icon(Icons.photo_album),
82-
// TODO: Not exactly a good practice.
83-
onPressed: () => NativeImagePickerMacOS().openPhotosApp(),
84-
),
79+
if (_useMacOSNativePicker)
80+
ElevatedButton.icon(
81+
label: Text('Open Photos App'),
82+
icon: Icon(Icons.photo_album),
83+
// Using instanceOrThrow since we assume the current instance
84+
// is NativeImagePickerMacOS due to _useMacOSNativePicker check.
85+
onPressed: () =>
86+
NativeImagePickerMacOS.instanceOrThrow.openPhotosApp(),
87+
),
8588
TextFormField(
8689
controller: _imageMaxWidthController,
8790
decoration: InputDecoration(labelText: 'Image Max Width'),

0 commit comments

Comments
 (0)