chore: bring flutter client, docs, deploy and tools into monorepo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 03:22:24 +02:00
co-authored by Claude Opus 4.8
parent 8c0519d125
commit 4b80c7dbb0
189 changed files with 19124 additions and 0 deletions
@@ -0,0 +1,493 @@
# Plugin Testing Guide
## Overview
Flutter plugins require special testing strategies because they include native code. This guide covers testing plugin packages with Dart, native, and integration tests.
## Test Types for Plugins
| Type | Tests | Runs On | Purpose |
|----------------|---------------------------|------------------|-----------------------------------|
| Dart Unit | Single classes/functions | Dart VM | Test Dart code in isolation |
| Dart Widget | UI components | Test environment | Test widget behavior |
| Dart Integration | Dart + Native bridge | Device/Emulator | Test full plugin functionality |
| Native Unit | Native code | Native test env | Test native code in isolation |
| Native UI | Native UI + Flutter UI | Device/Emulator | Test native UI interactions |
## Project Structure
```
my_plugin/
├── lib/ # Dart code
│ └── my_plugin.dart
├── android/ # Android native code
│ └── src/test/ # Android unit tests
├── ios/ # iOS native code
├── example/ # Example app
│ ├── integration_test/ # Integration tests
│ ├── ios/RunnerTests/ # iOS tests
│ └── lib/
├── test/ # Dart unit tests
│ └── my_plugin_test.dart
└── pubspec.yaml
```
## Dart Unit Tests
### Basic Plugin Unit Test
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';
void main() {
test('plugin initializes correctly', () {
final plugin = MyPlugin();
expect(plugin.initialized, false);
plugin.initialize();
expect(plugin.initialized, true);
});
test('processes data correctly', () {
final plugin = MyPlugin();
final result = plugin.processData([1, 2, 3]);
expect(result, [2, 4, 6]);
});
}
```
### Mocking Platform Channels in Unit Tests
```dart
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_plugin/my_plugin.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late MyPlugin plugin;
setUp(() {
// Mock platform channel
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
if (methodCall.method == 'getNativeValue') {
return 42;
}
return null;
},
);
plugin = MyPlugin();
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
null,
);
});
test('gets value from native side', () async {
final result = await plugin.getNativeValue();
expect(result, 42);
});
}
```
## Dart Integration Tests
### Setup
Add to `example/pubspec.yaml`:
```yaml
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
my_plugin:
path: ../
```
### Basic Integration Test
```dart
// example/integration_test/my_plugin_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_plugin_example/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('plugin works in real app', (tester) async {
await tester.pumpWidget(const MyApp());
final plugin = MyPlugin();
final result = await plugin.getNativeValue();
expect(result, greaterThan(0));
});
}
```
### Create Test Driver
```dart
// test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();
```
### Run Integration Tests
```bash
# From example directory
cd example
flutter test integration_test/
# Or with driver
flutter drive --target=integration_test/my_plugin_test.dart
```
## Android Unit Tests
### Setup
Create test in `android/src/test/java/com/example/my_plugin/MyPluginTest.java`:
```java
package com.example.my_plugin;
import org.junit.Test;
import static org.junit.Assert.*;
public class MyPluginTest {
@Test
public void testNativeMethod() {
MyPlugin plugin = new MyPlugin();
int result = plugin.calculate(10, 20);
assertEquals(30, result);
}
@Test
public void testStringProcessing() {
MyPlugin plugin = new MyPlugin();
String result = plugin.process("hello");
assertEquals("HELLO", result);
}
}
```
### Run Android Tests
```bash
# From android directory
cd example/android
# Run all tests
./gradlew testDebugUnitTest
# Run specific test
./gradlew test --tests MyPluginTest.testNativeMethod
```
## iOS Unit Tests
### Setup
Create test in `example/ios/RunnerTests/RunnerTests.m`:
```objectivec
#import <XCTest/XCTest.h>
#import "MyPlugin.h"
@interface MyPluginTest : XCTestCase
@end
@implementation MyPluginTest
- (void)setUp {
[super setUp];
// Setup code
}
- (void)testNativeMethod {
MyPlugin *plugin = [[MyPlugin alloc] init];
NSInteger result = [plugin calculateWithA:10 b:20];
XCTAssertEqual(result, 30);
}
- (void)testStringProcessing {
MyPlugin *plugin = [[MyPlugin alloc] init];
NSString *result = [plugin processString:@"hello"];
XCTAssertEqualObjects(result, @"HELLO");
}
@end
```
### Run iOS Tests
```bash
# From ios directory
cd example/ios
# Run tests
xcodebuild test -workspace Runner.xcworkspace -scheme Runner -configuration Debug
# Or run from Xcode
# Product > Test
```
## Native UI Tests (Espresso/XCUITest)
### Android Espresso Test
```java
// android/src/androidTest/java/com/example/my_plugin/UiTest.java
package com.example.my_plugin;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@RunWith(AndroidJUnit4.class)
public class UiTest {
@Rule
public ActivityTestRule<MainActivity> activityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void testPluginButton() {
onView(withText("Plugin Button"))
.check(matches(isDisplayed()));
}
}
```
### iOS XCUITest
```objectivec
// example/ios/RunnerUITests/RunnerUITests.m
#import <XCTest/XCTest.h>
@interface RunnerUITests : XCTestCase
@end
@implementation RunnerUITests
- (void)setUp {
[super setUp];
XCUIApplication *app = [[XCUIApplication alloc] init];
[app launch];
}
- (void)testPluginButtonExists {
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *button = app.buttons[@"Plugin Button"];
XCTAssertTrue(button.exists);
}
- (void)testPluginButtonClick {
XCUIApplication *app = [[XCUIApplication alloc] init];
XCUIElement *button = app.buttons[@"Plugin Button"];
[button tap];
XCUIElement *result = app.staticTexts[@"Success"];
XCTAssertTrue(result.exists);
}
@end
```
## Testing Plugin Initialization
### Test Plugin Registration
```dart
testWidgets('plugin registers correctly', (tester) async {
final calls = <MethodCall>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
calls.add(methodCall);
return null;
},
);
await tester.pumpWidget(const MyApp());
expect(calls.any((call) => call.method == 'initialize'), true);
});
```
## Testing Error Handling
### Native Error Handling
```dart
testWidgets('handles native errors', (tester) async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('com.example.my_plugin'),
(MethodCall methodCall) async {
if (methodCall.method == 'failingMethod') {
throw PlatformException(
code: 'ERROR',
message: 'Native error occurred',
);
}
return null;
},
);
final plugin = MyPlugin();
expect(
() => plugin.failingMethod(),
throwsA(isA<PlatformException>()),
);
});
```
## Testing Platform-Specific Features
### Android-Specific Tests
```java
@Test
public void testAndroidOnlyFeature() {
MyPlugin plugin = new MyPlugin();
// Test Android-specific API
Context context = InstrumentationRegistry.getTargetContext();
String deviceModel = plugin.getDeviceModel(context);
assertNotNull(deviceModel);
}
```
### iOS-Specific Tests
```objectivec
- (void)testIOSOnlyFeature {
MyPlugin *plugin = [[MyPlugin alloc] init];
// Test iOS-specific API
NSString *deviceName = plugin.getDeviceName();
XCTAssertNotNil(deviceName);
}
```
## Testing Multiple Platforms
### Platform Detection Test
```dart
testWidgets('works on all platforms', (tester) async {
final plugin = MyPlugin();
final platform = await plugin.getPlatform();
if (Platform.isAndroid) {
expect(platform, contains('Android'));
} else if (Platform.isIOS) {
expect(platform, contains('iOS'));
}
});
```
## Testing Performance
### Native Performance Test
```java
@Test
public void testPerformance() {
MyPlugin plugin = new MyPlugin();
long startTime = System.nanoTime();
plugin.heavyOperation();
long endTime = System.nanoTime();
long duration = endTime - startTime;
long maxDuration = 100_000_000; // 100ms
assertTrue("Operation too slow", duration < maxDuration);
}
```
## Best Practices
1. **Test at each layer** - Dart, native, and integration
2. **Mock platform channels** - In Dart unit tests
3. **Test error cases** - Native failures, network issues
4. **Use integration tests** - Test full plugin functionality
5. **Test on real devices** - For platform-specific features
6. **Keep tests isolated** - Each test should be independent
7. **Test all platforms** - Android, iOS, web, desktop
8. **Document platform limitations** - Note platform-specific behavior
## CI/CD for Plugins
### GitHub Actions
```yaml
name: Plugin Tests
on: [push, pull_request]
jobs:
dart-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v2
- run: flutter test
android-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run Android tests
run: |
cd example/android
./gradlew testDebugUnitTest
ios-tests:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run iOS tests
run: |
cd example/ios
xcodebuild test -workspace Runner.xcworkspace \
-scheme Runner -configuration Debug
```