An open API service indexing awesome lists of open source software.

https://github.com/moinsen-dev/moinsen_voyage


https://github.com/moinsen-dev/moinsen_voyage

Last synced: 11 months ago
JSON representation

Awesome Lists containing this project

README

          

# 🚒 Moinsen Voyage
[![pub package](https://img.shields.io/pub/v/moinsen_voyage.svg)](https://pub.dev/packages/moinsen_voyage)
[![Flutter](https://img.shields.io/badge/Flutter-%3E%3D3.0.0-blue)](https://flutter.dev)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> **Journey-based testing for Flutter that makes sense**
>
> Transform how you test Flutter apps by recording real user journeys and replaying them with intelligent variations. Features exception handling, comprehensive UI management, automated analysis, and production-ready reporting.

![Moinsen Voyage](moinsen_voyage.png)

## 🌟 Why Moinsen Voyage?

Picture this: You're manually testing your Flutter app, navigating through screens, tapping buttons, entering text. Everything works perfectly. But then you ask yourself - "Now I have to write all of this as a test?"

What if you didn't have to?

**Moinsen Voyage** records your manual testing sessions and transforms them into comprehensive, intelligent test suites. But it goes far beyond simple record-and-replay. It understands your app's state management, generates test data variations, monitors performance, provides perceptual golden testing, and even generates app store screenshots.

### The Problem with Traditional Testing

Traditional Flutter testing often feels like describing a journey in a foreign language. You write abstract commands that attempt to recreate user behavior, but the connection between what users actually do and what your tests check can be tenuous. Consider this traditional test:

```dart
// Traditional approach - disconnected from real usage
testWidgets('User can complete purchase', (tester) async {
await tester.pumpWidget(MyApp());
await tester.tap(find.byKey(Key('product_1')));
await tester.pump();
await tester.tap(find.byKey(Key('add_to_cart')));
await tester.pump();
// ... many more manual steps
});
```

This approach has several pain points that developers face daily:
- You write the same interaction twice (once manually, once in code)
- State management requires complex mocking and setup
- Edge cases are often missed
- Visual regressions require pixel-perfect matching that breaks easily
- Performance degradation goes unnoticed
- App store screenshots need separate tooling and manual updates

### The Voyage Approach

Moinsen Voyage reimagines testing as a journey. Instead of writing tests, you demonstrate them. The framework then intelligently transforms your demonstrations into comprehensive test coverage:

```dart
// The Voyage way - natural and comprehensive
testVoyage('User can complete purchase', (voyage) async {
// Record once, test everywhere
final journey = await voyage.loadJourney('purchase_flow');

// Automatically test with variations
await journey.replayWith([
// Different locales
VoyageContext.locale('de_DE'),
VoyageContext.locale('en_US'),

// Different data conditions
VoyageContext.outOfStock(),
VoyageContext.slowNetwork(),

// Different user types
VoyageContext.premiumUser(),
VoyageContext.guestUser(),
]);
});
```
## 🎯 Core Features

### 1. 🎬 Journey Recording & Replay

At the heart of Moinsen Voyage is a simple but powerful idea: your manual testing sessions are valuable data. The framework provides special widgets that act as observers, silently recording every interaction while you use your app naturally.

These aren't just macro recordings - they're intelligent captures that understand the semantic meaning of your actions. When you tap a "Buy Now" button, Voyage doesn't just record "tap at coordinates 150,300". It records "user initiated purchase action on product_detail_screen". This semantic understanding makes your tests resilient to UI changes.

**Enhanced Event Capture**: Our advanced capture system handles rapid user interactions flawlessly, ensuring no events are lost even during intensive user sessions.

### 2. πŸ›‘οΈ Exception Handling & Recovery

Moinsen Voyage includes a comprehensive exception handling system that catches errors gracefully without crashing your app. When exceptions occur, they're automatically recorded with full context and displayed with helpful error UIs.

```dart
// Automatic exception boundary with graceful fallback
VoyageCapture(
id: 'risky_widget',
captureExceptions: true, // Enabled by default
child: ProblematicWidget(),
errorBuilder: (exception) => CustomErrorWidget(exception),
)
```

**Exception Features**:
- **Automatic Recording**: All exceptions are captured with severity levels (warning, error, critical)
- **Graceful Fallback**: Shows helpful error UI instead of app crashes
- **Journey Integration**: Exceptions become part of your journey data for analysis
- **Automated Analysis**: Built-in exception analysis provides quality assessment and recommendations

### 3. πŸŽ›οΈ Comprehensive UI Management

Built-in UI components provide complete control over your testing experience without any setup complexity:

```dart
void main() {
runApp(
VoyageWrapper( // One wrapper gives you everything
child: MyApp(),
),
);
}
```

**UI Features**:
- **Journey Dashboard**: Visual journey management and inspection
- **Debug Badge**: Quick access to recording controls and settings
- **Test Integration**: Seamless replay functionality for Flutter tests
- **Settings Panel**: Configure all aspects of Voyage behavior
- **Journey Viewer**: Browse, search, and analyze recorded journeys

### 4. State Management Intelligence

One of the biggest challenges in testing stateful applications is ensuring your tests start from a known state and can verify state changes correctly. Moinsen Voyage speaks fluently with all major state management solutions:

**For Riverpod users**, Voyage automatically tracks provider dependencies and state evolution:
```dart
// Your normal Riverpod code
class CartScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return VoyageRecordable( // Just wrap your widget
journeyName: 'shopping_cart',
child: YourNormalWidget(cart: cart),
);
}
}
```

**For Bloc enthusiasts**, Voyage captures the complete event-state dance:
```dart
// Voyage automatically records bloc events and resulting states
VoyageBlocBuilder(
voyageId: 'cart_management',
builder: (context, state) {
// Your normal bloc builder code
},
)
```

**For Hooks practitioners**, every hook's lifecycle is tracked:
```dart
// Voyage-aware hooks that record their evolution
Widget build(BuildContext context) {
final controller = useVoyageTextController('user_input');
final animationState = useVoyageAnimationController('drawer_animation');
// Your normal hooks code
}
```

### 3. Intelligent Test Data Generation

Real-world testing requires real-world data. Voyage includes a sophisticated data generation system that understands context and relationships:

```dart
// Generate realistic test data for your domain
final testUser = await VoyageDataDock.generateUser(
locale: 'de_DE',
profile: UserProfile.casualShopper,
withPurchaseHistory: true,
);

// Voyage maintains data relationships
final testOrder = await VoyageDataDock.generateOrder(
forUser: testUser,
products: ProductCategory.electronics,
shipping: ShippingType.express,
);
```

The data generator isn't random - it's intelligent. It knows that German phone numbers follow different patterns than US ones, that email addresses should be deliverable for testing email flows, and that credit card numbers should pass Luhn validation while still being test-safe.

### 4. Golden Testing with Perceptual Comparison

Moinsen Voyage introduces a revolutionary approach to golden testing that uses perceptual image comparison. Instead of pixel-perfect matching that breaks with minor changes, our golden testing understands how humans perceive visual differences:

```dart
// Configure golden testing with human-like perception
VoyageRecordable(
journeyName: 'checkout_flow',
goldenConfig: GoldenConfig(
enabled: true,
captureStrategy: CaptureStrategy.smart,
tolerance: GoldenTolerance(
global: 0.01, // 1% difference allowed globally
regions: {
'dynamic_content': 0.05, // 5% for areas with dynamic content
},
),
),
child: CheckoutScreen(),
)

// During testing, golden comparison uses SSIM algorithm
testVoyage('Checkout UI remains visually consistent', (voyage) async {
final journey = await voyage.loadJourney('checkout_flow');
final result = await GoldenTestRunner.runGoldenTests(journey);

expect(result.passed, isTrue);
// If failed, approve changes with: GoldenApprovalWorkflow.reviewPendingUpdates()
});
```

The golden testing system features:
- **Perceptual Comparison**: Uses SSIM (Structural Similarity Index) for human-like comparison
- **Version Control**: Golden images are versioned with branch support
- **Approval Workflow**: Review and approve visual changes with side-by-side comparison
- **Smart Capture**: Automatically captures at optimal moments during journey replay

### 5. πŸ“Š Comprehensive Reporting & Analysis

Every journey generates detailed reports in multiple formats (JSON, Markdown, HTML) with comprehensive analysis:

```dart
// Generate detailed journey report
final report = await JourneyReporter.instance.generateReport(
journey: journey,
includeExceptionAnalysis: true,
includePerformanceMetrics: true,
includeVisualRegression: true,
);

// Save in multiple formats
await JourneyReporter.instance.saveReport(report, 'reports/journey.html');
await JourneyReporter.instance.saveReport(report, 'reports/journey.md');
```

**Reporting Features**:
- **Exception Analysis**: Automated quality assessment with recommendations
- **Performance Monitoring**: Frame times, memory usage, network metrics
- **Golden Testing**: Perceptual comparison results with approval workflow
- **Trend Analysis**: Track quality improvements/degradations over time
- **Multi-format Output**: HTML with graphs, Markdown for docs, JSON for CI/CD

### 6. Performance Lighthouse

Performance is a feature, and Voyage treats it as such. Every journey automatically includes performance profiling:

```dart
// Performance is monitored automatically during replay
final report = await journey.replay();

print(report.performance.summary);
// Average frame time: 12.3ms (81 FPS)
// Jank frames: 2 (0.5%)
// Memory growth: +2.1MB
// Network requests: 15 (avg 127ms)
```

### 7. App Store Screenshot Generation

Transform your test journeys into professional app store screenshots with automatic device framing, localization, and layout optimization:

```dart
// Mark screenshot points during journey recording
await VoyageCapture.appStoreScreenshot(
name: 'premium_features',
title: 'Unlock Premium Features',
subtitle: 'Get the most out of your app',
priority: 100, // Higher priority appears first in app stores
category: AppStoreCategory.mainFeature,
);

// Generate screenshots for all app stores
final result = await VoyageAppStore.generateScreenshots(
journey: 'complete_user_flow',
config: AppStoreGenerationConfig(
platforms: [AppStorePlatform.iOS, AppStorePlatform.android],
addDeviceFrames: true, // Adds realistic device bezels
statusBar: StatusBarStyles.promotional, // Perfect status bar
locales: [Locale('en'), Locale('de'), Locale('ja')], // Multi-language
devices: AppStoreDevices.required, // All required sizes
exportPath: 'app_store_assets/',
),
);

print('Generated ${result.totalGenerated} screenshots');
// Output structure:
// app_store_assets/
// ios/
// en/
// 01_mainfeature_iphone14promax.png
// 02_premium_iphone14promax.png
// de/
// 01_mainfeature_iphone14promax.png
// android/
// en/
// 01_mainfeature_pixel6.png
```

**App Store Features**:
- **Device Frames**: Automatic device frame rendering with accurate bezels and notches
- **Multi-locale Support**: Generate screenshots in all your supported languages
- **Smart Organization**: Exports organized by platform, locale, and priority
- **Status Bar Customization**: Professional status bars with full battery and signal
- **Batch Generation**: Generate hundreds of screenshots from a single journey

### Test-Driven App Store Screenshots

Take app store screenshot generation to the next level with Flutter test integration. Instead of relying on manual recordings, generate perfect screenshots programmatically in your test environment:

```dart
import 'package:moinsen_voyage/moinsen_voyage_test.dart';

testWidgets('generate app store screenshots', (tester) async {
await tester.pumpWidget(MyApp());

// Capture individual screenshots during your test
await tester.captureAppStoreScreenshot(
name: 'welcome_screen',
title: 'Welcome to My App',
subtitle: 'Get started in seconds',
category: AppStoreCategory.onboarding,
);

// Navigate through your app
await tester.tap(find.text('Get Started'));
await tester.pumpAndSettle();

// Capture another screenshot
await tester.captureAppStoreScreenshot(
name: 'main_features',
title: 'Powerful Features',
subtitle: 'Everything you need in one place',
category: AppStoreCategory.mainFeature,
);

// Or replay entire journeys with automatic screenshot generation
final result = await tester.replayJourneyWithScreenshots(
journeyId: 'user_onboarding_flow',
devices: [DeviceConfiguration.iPhone14ProMax, DeviceConfiguration.pixel6],
locales: [Locale('en'), Locale('de'), Locale('fr')],
config: TestAppStoreConfig.complete(),
);

expect(result.success, isTrue);
expect(result.screenshots.length, greaterThan(6)); // 2 devices Γ— 3 locales
});
```

**Test-Driven Benefits**:
- **Perfect Timing**: Screenshots captured at exactly the right moment
- **Consistent Quality**: No manual timing or coordination issues
- **Automation Ready**: Generate screenshots in CI/CD pipelines
- **Multi-Configuration**: Test multiple devices, locales, and scenarios automatically
- **Version Control**: Track screenshot changes alongside code changes
## πŸš€ Getting Started

### Installation

Adding Moinsen Voyage to your Flutter project is straightforward. Add the dependency to your `pubspec.yaml`:

```yaml
dependencies:
moinsen_voyage: ^0.1.0
```

Run `flutter pub get` to fetch the package.

### Setup in 30 Seconds

Getting started with Moinsen Voyage is incredibly simple. Just wrap your app with `VoyageWrapper`:

```dart
import 'package:flutter/foundation.dart';
import 'package:moinsen_voyage/moinsen_voyage.dart';

void main() {
// Initialize Voyage (debug/profile only)
VoyageConfig.initialize(
enabled: !kReleaseMode,
captureExceptions: true, // Handle exceptions gracefully
throttleTapEvents: true, // Handle rapid interactions
);

runApp(
VoyageWrapper( // This gives you everything!
child: MyApp(),
),
);
}
```

That's it! Your app now has:
- βœ… Journey recording capabilities
- βœ… Exception handling and recovery
- βœ… Built-in UI for managing recordings
- βœ… Debug badge for quick access
- βœ… Test-driven replay functionality

> **Architecture Note**: Moinsen Voyage maintains a clean separation between recording (available in debug/profile builds) and replay (test-only). This ensures your production app stays lean while your tests remain comprehensive.

### Record Your First Journey

Start recording by wrapping key screens with `VoyageRecordable`:

```dart
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VoyageRecordable(
journeyName: 'user_login',
child: Scaffold(
appBar: AppBar(title: Text('Login')),
body: LoginForm(),
),
);
}
}
```

For individual widgets, use `VoyageCapture`:

```dart
VoyageCapture(
id: 'login_button',
child: ElevatedButton(
onPressed: _handleLogin,
child: Text('Login'),
),
)
```

The debug badge (visible in debug mode) gives you instant access to:
- Start/stop recording
- View recorded journeys
- Access settings
- Replay journeys in tests

## πŸ”§ Enhanced Widget Support

Moinsen Voyage provides specialized widgets optimized for common UI patterns:

### VoyageTextField & VoyageTextFormField

Enhanced text input widgets with built-in recording and exception handling:

```dart
VoyageTextField(
id: 'search_field',
semanticLabel: 'Search products',
controller: _controller,
decoration: InputDecoration(labelText: 'Search'),
// Automatically records text changes and handles exceptions
)

VoyageTextFormField(
id: 'email_field',
validator: (value) => EmailValidator.validate(value),
// Captures validation errors as warning-level exceptions
)
```

### Advanced VoyageCapture Features

```dart
VoyageCapture(
id: 'complex_widget',

// Exception handling
captureExceptions: true,
errorBuilder: (exception) => ErrorWidget(exception),

// Interaction filtering
captureTaps: true,
captureTextInput: true,
captureScroll: false,

// Metadata for context
metadata: {'feature': 'checkout', 'priority': 'critical'},

child: ComplexWidget(),
)
```

### Test-Only Replay

Voyage replay functionality is designed exclusively for use in Flutter tests, ensuring clean separation between recording (production) and replay (testing):

```dart
// In your test files
import 'package:moinsen_voyage/moinsen_voyage_test.dart';

testWidgets('replay user journey', (tester) async {
await tester.pumpWidget(MyApp());

// Simple replay using extension method
await tester.replayJourney('journey_id');

// Or use the test driver for more control
final driver = VoyageTestDriver(tester: tester);
await driver.replayJourney('journey_id',
speedMultiplier: 10.0,
onError: (error) => debugPrint('Error: $error'),
);
});
```

## πŸ” Exception Analysis & Quality Assessment

Moinsen Voyage includes sophisticated tools for analyzing exceptions and assessing journey quality:

### Automated Exception Analysis

```dart
// Analyze a journey for exception patterns
final analysis = JourneyExceptionAnalyzer.instance.analyzeJourney(journey);

print('Quality Level: ${analysis.qualityLevel}'); // excellent, good, fair, poor, critical
print('Passed: ${analysis.passed}'); // true/false
print('Issues Found: ${analysis.issues.length}');

// Get actionable recommendations
final recommendations = JourneyExceptionAnalyzer.instance
.generateRecommendations(analysis);

for (final rec in recommendations) {
print('πŸ“‹ $rec');
}
// Output: 🚨 URGENT: Fix critical exceptions immediately
// πŸ› οΈ Focus on error-prone widgets: login_form, checkout_button
```

### Exception Quality Rules

Configure analysis rules for different environments:

```dart
// Strict rules for production
final strictRules = ExceptionAnalysisRules.strict();

// Standard rules for development
final standardRules = ExceptionAnalysisRules.standard();

// Lenient rules for experimental features
final lenientRules = ExceptionAnalysisRules.lenient();

final analysis = JourneyExceptionAnalyzer.instance.analyzeJourney(
journey,
rules: strictRules,
);
```

### Trend Analysis

Track exception trends across multiple journeys:

```dart
final journeys = await JourneyManager.loadRecentJourneys(30);
final trends = JourneyExceptionAnalyzer.instance.analyzeTrends(journeys);

print('Trend: ${trends.trend}'); // improving, stable, worsening
print('Description: ${trends.trendDescription}');
```

### Your First Recording

Now comes the exciting part - recording your first journey. Let's walk through a complete example that demonstrates the core concepts:

```dart
// Wrap your screen with VoyageRecordable
class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VoyageRecordable(
journeyName: 'user_login_flow',
metadata: {
'feature': 'authentication',
'priority': 'critical',
},
child: Scaffold(
appBar: AppBar(title: Text('Login')),
body: LoginForm(),
),
);
}
}

// For individual widgets, use VoyageCapture
class LoginForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
VoyageCapture(
id: 'email_field',
semantic: 'user_email_input',
child: TextField(
decoration: InputDecoration(labelText: 'Email'),
),
),
VoyageCapture(
id: 'password_field',
semantic: 'user_password_input',
child: TextField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
),
),
VoyageCapture(
id: 'login_button',
semantic: 'submit_login',
child: ElevatedButton(
onPressed: () => _performLogin(context),
child: Text('Login'),
),
),
],
);
}
}
```

Notice how each `VoyageCapture` has both an `id` and a `semantic` property. The `id` helps Voyage find the widget during replay, while `semantic` describes what the widget does in user terms. This dual identification makes your tests both reliable and readable.
### Recording Your Journey

Once your widgets are wrapped, recording happens automatically when you use your app. But let's understand what's happening behind the scenes, as this knowledge will help you create better tests.

When you interact with a VoyageRecordable widget, the framework creates a timeline of events. Think of it like a ship's log, but instead of "10:00 - Spotted land", it records "10:00:00.123 - User tapped login_button while app was in state: not_authenticated". Here's how to control and monitor your recordings:

```dart
// Start a new recording session programmatically
VoyageRecorder.startSession(
name: 'complete_purchase_flow',
description: 'Happy path from product browse to order confirmation',
tags: ['e-commerce', 'critical-path', 'payment'],
);

// Your manual interactions are now being recorded...

// You can add contextual checkpoints
VoyageRecorder.addCheckpoint('cart_loaded', {
'item_count': 3,
'total_value': 156.99,
'currency': 'EUR',
});

// End the recording
await VoyageRecorder.endSession();
```

The recording captures multiple layers of information simultaneously. At the interaction layer, it records every tap, swipe, and text entry. At the state layer, it captures how your providers, blocs, or hooks change in response. At the visual layer, it takes intelligent snapshots at key moments. And at the performance layer, it monitors frame times and memory usage.

### Replaying Journeys in Tests

Now comes the powerful part - turning your recordings into comprehensive tests. The beauty of Moinsen Voyage is that one recording can generate dozens of test scenarios. Let's explore how this works:

```dart
import 'package:moinsen_voyage/moinsen_voyage_test.dart';

void main() {
group('User Authentication Flows', () {
testVoyage('Login works with valid credentials', (voyage) async {
// Load the recorded journey
final journey = await voyage.loadJourney('user_login_flow');

// Replay with original data - this should always pass
final result = await journey.replay();
expect(result.success, isTrue);
expect(result.finalRoute, equals('/home'));
});

testVoyage('Login handles various user scenarios', (voyage) async {
final journey = await voyage.loadJourney('user_login_flow');

// Here's where the magic happens - test variations
final results = await journey.replayWithVariations([
// Test with different locales
VoyageVariation.locale(['de_DE', 'fr_FR', 'ja_JP']),

// Test with edge case data
VoyageVariation.textInput(
'email_field',
['test@example.com', 'user+tag@domain.com', 'vΓ©ry.ΓΌnusual@email.com'],
),

// Test with network conditions
VoyageVariation.network([
NetworkCondition.fast3G(),
NetworkCondition.slow3G(),
NetworkCondition.offline(),
]),

// Test with different device sizes
VoyageVariation.deviceSize([
DeviceSize.iPhone13Mini(),
DeviceSize.iPadPro(),
DeviceSize.pixel5(),
]),
]);

// Analyze results across all variations
expect(results.where((r) => r.success).length,
greaterThan(results.length * 0.9),
reason: 'At least 90% of variations should succeed');
});
});
}
```

Notice how one recording becomes a comprehensive test suite. The framework intelligently combines variations, so if you specify 3 locales, 3 email formats, 3 network conditions, and 3 device sizes, you're actually running 81 different test scenarios - all from one recording!
## πŸŽ“ Understanding State Management Integration

One of the most challenging aspects of testing Flutter applications is dealing with state management. Each state management solution has its own patterns, and traditionally, this means learning different testing approaches for each one. Moinsen Voyage changes this by providing a unified abstraction layer that understands the unique characteristics of each state management approach while presenting a consistent testing interface.

Let's explore how this works by examining each supported state management solution in detail.

### Riverpod Integration

Riverpod's power lies in its compile-safe dependency injection and reactive state management. When testing Riverpod applications, the challenge is often in setting up the right provider overrides and ensuring state consistency. Moinsen Voyage handles this complexity for you:

```dart
// First, let's see how to make your Riverpod app voyage-aware
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return VoyageProviderScope( // Wrap your ProviderScope
child: ProviderScope(
child: MaterialApp(
home: HomeScreen(),
),
),
);
}
}

// In your widgets, use VoyageConsumer instead of Consumer
class ProductListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VoyageConsumer(
voyageId: 'product_list',
builder: (context, ref, child) {
final products = ref.watch(productsProvider);
final cartItemCount = ref.watch(cartProvider.select((cart) => cart.items.length));

// Voyage automatically records:
// 1. Which providers are being watched
// 2. Their values at each render
// 3. What triggers re-renders

return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ProductTile(products[index]),
);
},
);
}
}

// During replay, Voyage can inject different provider states
testVoyage('Products load correctly in various states', (voyage) async {
final journey = await voyage.loadJourney('browse_products');

await journey.replayWithProviderStates([
// Test with empty product list
ProviderState(productsProvider, []),

// Test with single product
ProviderState(productsProvider, [testProduct]),

// Test with many products (pagination)
ProviderState(productsProvider, generateProducts(100)),

// Test with error state
ProviderState(productsProvider, AsyncError('Network error')),
]);
});
```

The key insight here is that Voyage doesn't just record what providers were accessed, but also captures the entire dependency graph. If `productsProvider` depends on `userProvider` and `settingsProvider`, Voyage records those relationships, ensuring your tests accurately reflect the real behavior of your app.

### Bloc Integration

Bloc's event-driven architecture presents different testing challenges. With Bloc, you need to ensure that the right events produce the right states in the right order. Moinsen Voyage embraces Bloc's patterns while simplifying the testing process:

```dart
// Make your Bloc widgets voyage-aware
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return VoyageBlocBuilder(
voyageId: 'shopping_cart',
builder: (context, state) {
// Voyage records:
// 1. The sequence of events dispatched
// 2. The resulting states
// 3. The timing between events and state changes

if (state is CartLoading) {
return CircularProgressIndicator();
} else if (state is CartLoaded) {
return CartItemsList(items: state.items);
} else if (state is CartError) {
return ErrorMessage(state.message);
}
return EmptyCart();
},
);
}
}

// During testing, replay bloc event sequences
testVoyage('Cart behaves correctly with various event sequences', (voyage) async {
final journey = await voyage.loadJourney('shopping_cart_flow');

// Test different event sequences
await journey.replayWithBlocEvents([
// Normal flow
[AddToCart(productId: '123'), RemoveFromCart(productId: '123')],

// Rapid additions
List.generate(10, (i) => AddToCart(productId: 'product_$i')),

// Error scenarios
[AddToCart(productId: 'invalid'), ClearCart()],
]);
});
```

What makes this powerful is that Voyage understands Bloc's async nature. It knows that events don't immediately produce states, and it captures the complete timeline of when events are dispatched and when states change in response.
### Flutter Hooks Integration

Flutter Hooks brings functional programming patterns to widget state management. The challenge with testing hooks is that they encapsulate state in a way that's typically invisible to the outside world. Moinsen Voyage provides specialized hooks that maintain the same API while adding recording capabilities:

```dart
// Use Voyage-aware hooks in your widgets
class SearchScreen extends HookWidget {
@override
Widget build(BuildContext context) {
// These hooks work exactly like regular hooks but with recording capabilities
final searchQuery = useVoyageState('', stateId: 'search_query');
final searchResults = useVoyageFuture(
searchApi(searchQuery.value),
futureId: 'search_results',
);
final isLoading = useVoyageState(false, stateId: 'loading_state');

return Column(
children: [
VoyageCapture(
id: 'search_input',
child: TextField(
onChanged: (value) {
searchQuery.value = value;
isLoading.value = true;
},
decoration: InputDecoration(
labelText: 'Search products',
suffixIcon: isLoading.value ? CircularProgressIndicator() : null,
),
),
),
Expanded(
child: searchResults.when(
data: (results) => ResultsList(results),
loading: () => LoadingIndicator(),
error: (error, stack) => ErrorDisplay(error),
),
),
],
);
}
}
```

The beauty of this approach is that hook state, which is normally ephemeral and local to the widget, becomes observable and replayable without changing how you write your components.

## πŸ”¬ Advanced Features

Now that we've covered the basics, let's explore some of the more sophisticated capabilities that make Moinsen Voyage a comprehensive testing solution. These features address real-world testing challenges that teams face when building production Flutter applications.

### Intelligent Test Data Generation

Real applications deal with real data, and testing with oversimplified data often misses critical edge cases. Moinsen Voyage includes a sophisticated data generation system that understands your domain and generates realistic test scenarios. Let's explore how this works:

```dart
// Define your domain models with generation hints
@VoyageDataModel()
class User {
final String id;
final String name;
final String email;
final DateTime birthDate;
final Address address;
final List paymentMethods;

User({
required this.id,
required this.name,
required this.email,
required this.birthDate,
required this.address,
required this.paymentMethods,
});
}

// Generate test data that makes sense
final testUsers = await VoyageDataDock.generateUsers(
count: 100,
constraints: UserConstraints(
locales: ['de_DE', 'en_US', 'fr_FR'],
ageRange: AgeRange(18, 65),
includeEdgeCases: true, // Very old, very young, special characters in names
),
);

// The generator understands relationships
final orders = await VoyageDataDock.generateOrderHistory(
forUsers: testUsers,
ordersPerUser: Range(0, 20), // Some users have no orders
dateRange: LastDays(365),
productCategories: ['Electronics', 'Books', 'Clothing'],
);

// Use in your tests to ensure comprehensive coverage
testVoyage('Checkout works for diverse user profiles', (voyage) async {
final journey = await voyage.loadJourney('checkout_flow');

for (final user in testUsers.take(10)) { // Test with 10 random users
await journey.replayWithContext(
user: user,
cart: VoyageDataDock.generateCart(forUser: user),
);
}
});
```

What makes this powerful is that the data generator understands context. German users get German addresses with proper postal codes. Email addresses follow realistic patterns. Credit card numbers pass validation but are clearly test data. This attention to detail catches bugs that simple test data would miss.

### Performance Monitoring and Baselines

Performance is a feature, not an afterthought. Moinsen Voyage treats performance testing as a first-class citizen, automatically collecting metrics during every test run. Here's how to leverage this for maintaining app performance:

```dart
// Set performance baselines for critical journeys
await VoyagePerformance.setBaseline(
journeyName: 'app_startup',
metrics: PerformanceMetrics(
maxStartupTime: Duration(seconds: 2),
maxMemoryGrowth: 50 * 1024 * 1024, // 50MB
maxJankFrames: 5,
minFPS: 55,
),
);

// During testing, performance is automatically monitored
testVoyage('App maintains performance standards', (voyage) async {
final journey = await voyage.loadJourney('app_startup');
final result = await journey.replay();

// Voyage automatically compares against baselines
expect(result.performance.meetsBaseline, isTrue);

// Get detailed performance insights
print('Startup time: ${result.performance.startupTime}');
print('Memory usage: ${result.performance.peakMemory}');
print('Frame performance: ${result.performance.frameStats}');

// Generate performance report
await voyage.generatePerformanceReport(
outputPath: 'reports/performance.html',
includeGraphs: true,
compareWithPrevious: 5, // Compare with last 5 runs
);
});
```
### Golden Testing in Practice

Our golden testing system uses perceptual comparison to understand visual changes the way humans do. This eliminates false positives while catching real issues:

```dart
// Configure golden testing with perceptual understanding
final goldenConfig = GoldenConfig(
enabled: true,
captureStrategy: CaptureStrategy.smart, // Captures at optimal moments
comparisonConfig: GoldenComparisonConfig(
algorithm: ComparisonAlgorithm.ssim, // Perceptual comparison
tolerance: GoldenTolerance(
global: 0.01, // 1% global difference allowed
regions: {
'dynamic_content': 0.05, // 5% for changing content
'animations': 0.10, // 10% for animated areas
},
),
ignoreBoundingBoxes: [
'timestamp', 'loading_indicator', // Ignore specific areas
],
),
// Multi-device and theme variations
deviceVariations: [
DeviceConfiguration.iPhone14ProMax,
DeviceConfiguration.pixel6,
],
themeVariations: [ThemeMode.light, ThemeMode.dark],
localeVariations: ['en', 'de', 'ja'],
);

// Run golden tests with approval workflow
testVoyage('UI remains visually consistent', (voyage) async {
final journey = await voyage.loadJourney('critical_user_flows');

// Run golden tests across all variations
final result = await GoldenTestRunner.runGoldenTests(
journey,
config: goldenConfig,
);

if (!result.passed) {
// Review and approve changes interactively
final approval = await GoldenApprovalWorkflow.reviewPendingUpdates(
result.failures,
showSideBySide: true,
highlightDifferences: true,
);

if (approval.approved) {
await GoldenStorage.instance.promoteToBaseline(approval.updates);
print('Golden images updated successfully');
}
}
});
```

The system uses SSIM (Structural Similarity Index) to compare images the way humans perceive them. A slight color shift that's imperceptible to users won't fail your tests, but a button moving to a different location will be caught immediately.

## 🌍 Real-World Example: E-Commerce Checkout

Let's put it all together with a comprehensive example that shows how Moinsen Voyage transforms testing for a complex feature like e-commerce checkout. This example demonstrates how the various features work together to provide comprehensive test coverage:

```dart
// First, set up your checkout screen with voyage awareness
class CheckoutScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
final user = ref.watch(currentUserProvider);
final paymentMethod = ref.watch(selectedPaymentProvider);

return VoyageRecordable(
journeyName: 'checkout_flow',
metadata: {
'feature': 'ecommerce',
'criticalPath': true,
'estimatedDuration': '2-3 minutes',
},
child: Scaffold(
appBar: AppBar(title: Text('Checkout')),
body: CheckoutFlow(
cart: cart,
user: user,
paymentMethod: paymentMethod,
),
),
);
}
}

// Now, create a comprehensive test suite from a single recording
void main() {
group('E-Commerce Checkout Journey', () {
setUpAll(() async {
// Define performance baselines
await VoyagePerformance.setBaseline(
'checkout_flow',
maxDuration: Duration(seconds: 3),
maxMemoryGrowth: 20 * 1024 * 1024,
);
});

testVoyage('Checkout works across all scenarios', (voyage) async {
final journey = await voyage.loadJourney('checkout_flow');

// Generate comprehensive test scenarios
final scenarios = await VoyageScenarioGenerator.generate(
journey: journey,
dimensions: [
// User dimensions
TestDimension.users([
UserType.guest,
UserType.firstTime,
UserType.returning,
UserType.premium,
]),

// Cart dimensions
TestDimension.cartSizes([1, 5, 20, 100]), // Edge case: large cart
TestDimension.productTypes([
'physical',
'digital',
'mixed',
'subscription',
]),

// Payment dimensions
TestDimension.paymentMethods([
'credit_card',
'paypal',
'bank_transfer',
'crypto', // Edge case
]),

// Environmental dimensions
TestDimension.locales(['de_DE', 'en_US', 'ja_JP']),
TestDimension.devices([
DeviceProfile.iPhone13,
DeviceProfile.iPadPro,
DeviceProfile.pixel5,
]),
TestDimension.networkConditions([
NetworkProfile.wifi,
NetworkProfile.mobile3G,
NetworkProfile.flaky, // Intermittent connection
]),
],
);

// Run all scenarios with intelligent batching
final results = await voyage.runScenarios(
scenarios,
parallel: 4, // Run 4 scenarios simultaneously
stopOnFailure: false, // Continue even if some fail
generateReport: true,
);

// Analyze results
expect(results.successRate, greaterThan(0.95),
reason: 'At least 95% of scenarios should succeed');

// Check performance didn't degrade
expect(results.performanceRegressions, isEmpty);

// Ensure golden tests pass for critical UI elements
expect(results.goldenTestFailures, isEmpty);

// Generate comprehensive report
await voyage.generateReport(
'reports/checkout_analysis.html',
includeSuccesses: false, // Focus on failures
includePerformanceGraphs: true,
includeVisualDiffs: true,
groupBy: ReportGrouping.byFailureType,
);
});
});
}
```

This example showcases how one manual test recording becomes hundreds of automated test scenarios, each checking different aspects of your application. The framework handles the complexity of combining dimensions, managing test data, and analyzing results, letting you focus on what matters: ensuring your app works flawlessly for all users.

## πŸ› οΈ Troubleshooting

### Recording Not Starting

If your journeys aren't being recorded, check these common causes:

```dart
// Ensure Voyage is initialized and enabled
void main() {
VoyageConfig.initialize(
enabled: !kReleaseMode, // Must be true for recording
captureExceptions: true,
);

runApp(VoyageWrapper(child: MyApp())); // VoyageWrapper required
}

// Debug recording status
print('Voyage enabled: ${VoyageConfig.isEnabled}');
print('Exception capture: ${VoyageConfig.captureExceptions}');
```

### Fast Interactions Not Captured

If rapid taps aren't being recorded:

```dart
VoyageConfig.initialize(
throttleTapEvents: false, // Disable if you need every single tap
tapThrottleDurationMs: 100, // Reduce throttle duration
);
```

### Exceptions Not Being Handled

Ensure exception handling is enabled:

```dart
// Global configuration
VoyageConfig.initialize(captureExceptions: true);

// Per-widget override
VoyageCapture(
captureExceptions: true, // Override global setting
child: YourWidget(),
)
```

### UI Not Appearing

If the Voyage UI (badge, dashboard) doesn't appear:

```dart
// Ensure you're using VoyageWrapper
runApp(VoyageWrapper(child: MyApp()));

// And running in debug mode
assert(() {
print('Debug mode confirmed');
return true;
}());
```

### Performance Issues

If you experience performance problems:

```dart
VoyageConfig.initialize(
throttleTapEvents: true, // Reduce event frequency
captureExceptions: false, // Disable if not needed
);
```

## 🀝 Contributing

We believe that great testing tools are built by the community, for the community. Moinsen Voyage is designed to be extensible, and we welcome contributions that make Flutter testing better for everyone.

### How to Contribute

The best contributions come from real-world usage. If you're using Moinsen Voyage and encounter a scenario it doesn't handle well, that's valuable feedback. Here's how you can help:

1. **Report Issues**: Found a bug? Let us know! Please include a minimal reproduction case.
2. **Suggest Features**: Have an idea that would make testing easier? Open a discussion!
3. **Submit PRs**: Fixed a bug or added a feature? We'd love to review your code.
4. **Write Documentation**: Help others by improving our docs or writing tutorials.
5. **Create Plugins**: Extend Voyage for your specific needs and share with the community.

### Development Setup

To contribute to Moinsen Voyage, clone the repository and set up your development environment:

```bash
git clone https://github.com/moinsen/moinsen_voyage.git
cd moinsen_voyage
flutter pub get
flutter test # Run the test suite
```

See our [Implementation Guidelines](docs/IMPLEMENTATION_GUIDELINES.md) for detailed development practices.

## πŸ“œ License

Moinsen Voyage is released under the MIT License. See the [LICENSE](LICENSE) file for details.

## πŸ™ Acknowledgments

Moinsen Voyage stands on the shoulders of giants. We're grateful to the Flutter team for creating such an amazing framework, and to the broader Flutter community for pushing the boundaries of what's possible in mobile development.

Special thanks to the teams behind Riverpod, Bloc, and Flutter Hooks for creating state management solutions that make Flutter development a joy.

---

**Ready to transform your Flutter testing?** Start your voyage today and discover how natural testing can be when your tools understand your application as well as you do.

_Moinsen!_ πŸ‘‹