diff --git a/lib/data/firebase/firebase_service.dart b/lib/data/firebase/firebase_service.dart index abd8c1d..682b1b7 100644 --- a/lib/data/firebase/firebase_service.dart +++ b/lib/data/firebase/firebase_service.dart @@ -7,7 +7,6 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import '../local/local_storage_service.dart'; import '../local/models/item.dart'; -import '../session/models/user.dart'; import 'models/firebase_config.dart'; /// Exception thrown when Firebase operations fail. @@ -23,17 +22,17 @@ class FirebaseException implements Exception { } /// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics). -/// +/// /// This service provides: /// - Cloud Firestore for optional metadata sync and backup /// - Firebase Storage for optional media storage /// - Firebase Authentication for user login/logout /// - Firebase Cloud Messaging for push notifications /// - Firebase Analytics for optional analytics -/// +/// /// The service is modular and optional - can be enabled/disabled without affecting other modules. /// When disabled, all methods return safely without throwing errors. -/// +/// /// The service maintains offline-first behavior by syncing with local storage /// and only using Firebase as an optional cloud backup/sync layer. class FirebaseService { @@ -65,7 +64,7 @@ class FirebaseService { firebase_auth.User? _firebaseUser; /// Creates a [FirebaseService] instance. - /// + /// /// [config] - Firebase configuration (determines which services are enabled). /// [localStorage] - Local storage service for offline-first behavior. FirebaseService({ @@ -83,10 +82,10 @@ class FirebaseService { bool get isLoggedIn => _auth != null && _firebaseUser != null; /// Initializes Firebase services based on configuration. - /// + /// /// Must be called before using any Firebase services. /// If Firebase is disabled, this method does nothing. - /// + /// /// Throws [FirebaseException] if initialization fails. Future initialize() async { if (!config.enabled) { @@ -141,12 +140,12 @@ class FirebaseService { } /// Logs in a user with email and password. - /// + /// /// [email] - User email address. /// [password] - User password. - /// + /// /// Returns the Firebase Auth user. - /// + /// /// Throws [FirebaseException] if auth is disabled or login fails. Future loginWithEmailPassword({ required String email, @@ -157,7 +156,8 @@ class FirebaseService { } if (!_initialized || _auth == null) { - throw FirebaseException('Firebase not initialized. Call initialize() first.'); + throw FirebaseException( + 'Firebase not initialized. Call initialize() first.'); } try { @@ -173,7 +173,7 @@ class FirebaseService { } /// Logs out the current user. - /// + /// /// Throws [FirebaseException] if auth is disabled or logout fails. Future logout() async { if (!config.enabled || !config.authEnabled) { @@ -181,7 +181,8 @@ class FirebaseService { } if (!_initialized || _auth == null) { - throw FirebaseException('Firebase not initialized. Call initialize() first.'); + throw FirebaseException( + 'Firebase not initialized. Call initialize() first.'); } try { @@ -193,9 +194,9 @@ class FirebaseService { } /// Syncs local items to Firestore (cloud backup). - /// + /// /// [userId] - User ID to associate items with (for multi-user support). - /// + /// /// Throws [FirebaseException] if Firestore is disabled or sync fails. Future syncItemsToFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { @@ -203,7 +204,8 @@ class FirebaseService { } if (!_initialized || _firestore == null) { - throw FirebaseException('Firestore not initialized. Call initialize() first.'); + throw FirebaseException( + 'Firestore not initialized. Call initialize() first.'); } try { @@ -212,7 +214,8 @@ class FirebaseService { // Batch write to Firestore final batch = _firestore!.batch(); - final collection = _firestore!.collection('users').doc(userId).collection('items'); + final collection = + _firestore!.collection('users').doc(userId).collection('items'); for (final item in items) { final docRef = collection.doc(item.id); @@ -231,9 +234,9 @@ class FirebaseService { } /// Syncs items from Firestore to local storage. - /// + /// /// [userId] - User ID to fetch items for. - /// + /// /// Throws [FirebaseException] if Firestore is disabled or sync fails. Future syncItemsFromFirestore(String userId) async { if (!config.enabled || !config.firestoreEnabled) { @@ -241,7 +244,8 @@ class FirebaseService { } if (!_initialized || _firestore == null) { - throw FirebaseException('Firestore not initialized. Call initialize() first.'); + throw FirebaseException( + 'Firestore not initialized. Call initialize() first.'); } try { @@ -272,12 +276,12 @@ class FirebaseService { } /// Uploads a file to Firebase Storage. - /// + /// /// [file] - File to upload. /// [path] - Storage path (e.g., 'users/userId/media/image.jpg'). - /// + /// /// Returns the download URL. - /// + /// /// Throws [FirebaseException] if Storage is disabled or upload fails. Future uploadFile(File file, String path) async { if (!config.enabled || !config.storageEnabled) { @@ -285,7 +289,8 @@ class FirebaseService { } if (!_initialized || _storage == null) { - throw FirebaseException('Firebase Storage not initialized. Call initialize() first.'); + throw FirebaseException( + 'Firebase Storage not initialized. Call initialize() first.'); } try { @@ -298,7 +303,7 @@ class FirebaseService { } /// Gets the FCM token for push notifications. - /// + /// /// Returns the FCM token, or null if messaging is disabled. Future getFcmToken() async { if (!config.enabled || !config.messagingEnabled || _messaging == null) { @@ -313,12 +318,13 @@ class FirebaseService { } /// Logs an event to Firebase Analytics. - /// + /// /// [eventName] - Name of the event. /// [parameters] - Optional event parameters. - /// + /// /// Does nothing if Analytics is disabled. - Future logEvent(String eventName, {Map? parameters}) async { + Future logEvent(String eventName, + {Map? parameters}) async { if (!config.enabled || !config.analyticsEnabled || _analytics == null) { return; } @@ -327,7 +333,8 @@ class FirebaseService { // Convert Map to Map for Firebase Analytics Map? analyticsParams; if (parameters != null) { - analyticsParams = parameters.map((key, value) => MapEntry(key, value as Object)); + analyticsParams = + parameters.map((key, value) => MapEntry(key, value as Object)); } await _analytics!.logEvent( @@ -340,7 +347,7 @@ class FirebaseService { } /// Disposes of Firebase resources. - /// + /// /// Should be called when the service is no longer needed. Future dispose() async { if (_auth != null) { @@ -350,4 +357,3 @@ class FirebaseService { _initialized = false; } } - diff --git a/lib/data/immich/immich_service.dart b/lib/data/immich/immich_service.dart index 58698c1..05644ed 100644 --- a/lib/data/immich/immich_service.dart +++ b/lib/data/immich/immich_service.dart @@ -175,8 +175,6 @@ class ImmichService { } final assetsJson = assetsData['items'] as List; - final total = assetsData['total'] as int? ?? 0; - final count = assetsData['count'] as int? ?? 0; final List assets = assetsJson .map((json) => ImmichAsset.fromJson(json as Map)) diff --git a/lib/data/sync/sync_engine.dart b/lib/data/sync/sync_engine.dart index 1be63c4..9577d45 100644 --- a/lib/data/sync/sync_engine.dart +++ b/lib/data/sync/sync_engine.dart @@ -1,10 +1,7 @@ import 'dart:async'; import '../local/local_storage_service.dart'; -import '../local/models/item.dart'; import '../immich/immich_service.dart'; -import '../immich/models/immich_asset.dart'; import '../nostr/nostr_service.dart'; -import '../nostr/models/nostr_event.dart'; import '../nostr/models/nostr_keypair.dart'; import 'models/sync_status.dart'; import 'models/sync_operation.dart'; @@ -22,13 +19,13 @@ class SyncException implements Exception { } /// Engine for coordinating data synchronization between local storage, Immich, and Nostr. -/// +/// /// This service provides: /// - Bidirectional sync between local storage, Immich, and Nostr /// - Conflict resolution strategies /// - Offline queue for operations when network is unavailable /// - Automatic retry with exponential backoff -/// +/// /// The service is modular and UI-independent, designed for offline-first behavior. class SyncEngine { /// Local storage service. @@ -50,7 +47,8 @@ class SyncEngine { SyncOperation? _currentOperation; /// Stream controller for sync status updates. - final StreamController _statusController = StreamController.broadcast(); + final StreamController _statusController = + StreamController.broadcast(); /// Whether the engine has been disposed. bool _isDisposed = false; @@ -62,7 +60,7 @@ class SyncEngine { final int maxQueueSize; /// Creates a [SyncEngine] instance. - /// + /// /// [localStorage] - Local storage service (required). /// [immichService] - Immich service (optional). /// [nostrService] - Nostr service (optional). @@ -97,7 +95,9 @@ class SyncEngine { /// Gets the current queue of pending operations. List getPendingOperations() { - return _operationQueue.where((op) => op.status == SyncStatus.pending).toList(); + return _operationQueue + .where((op) => op.status == SyncStatus.pending) + .toList(); } /// Gets all operations (pending, in-progress, completed, failed). @@ -106,15 +106,15 @@ class SyncEngine { } /// Queues a sync operation. - /// + /// /// [operation] - The sync operation to queue. - /// + /// /// Throws [SyncException] if queue is full. void queueOperation(SyncOperation operation) { if (_isDisposed) { throw SyncException('SyncEngine has been disposed'); } - + if (_operationQueue.length >= maxQueueSize) { throw SyncException('Sync queue is full (max: $maxQueueSize)'); } @@ -129,12 +129,13 @@ class SyncEngine { } /// Syncs an item from local storage to Immich. - /// + /// /// [itemId] - The ID of the item to sync. /// [priority] - Priority of the sync operation. - /// + /// /// Returns the sync operation ID. - Future syncToImmich(String itemId, {SyncPriority priority = SyncPriority.normal}) async { + Future syncToImmich(String itemId, + {SyncPriority priority = SyncPriority.normal}) async { if (_immichService == null) { throw SyncException('Immich service not configured'); } @@ -153,12 +154,13 @@ class SyncEngine { } /// Syncs metadata from Immich to local storage. - /// + /// /// [assetId] - The Immich asset ID to sync. /// [priority] - Priority of the sync operation. - /// + /// /// Returns the sync operation ID. - Future syncFromImmich(String assetId, {SyncPriority priority = SyncPriority.normal}) async { + Future syncFromImmich(String assetId, + {SyncPriority priority = SyncPriority.normal}) async { if (_immichService == null) { throw SyncException('Immich service not configured'); } @@ -177,12 +179,13 @@ class SyncEngine { } /// Syncs metadata to Nostr. - /// + /// /// [itemId] - The ID of the item to sync. /// [priority] - Priority of the sync operation. - /// + /// /// Returns the sync operation ID. - Future syncToNostr(String itemId, {SyncPriority priority = SyncPriority.normal}) async { + Future syncToNostr(String itemId, + {SyncPriority priority = SyncPriority.normal}) async { if (_nostrService == null) { throw SyncException('Nostr service not configured'); } @@ -205,11 +208,12 @@ class SyncEngine { } /// Performs a full sync: syncs all items between configured services. - /// + /// /// [priority] - Priority of sync operations. - /// + /// /// Returns a list of operation IDs. - Future> syncAll({SyncPriority priority = SyncPriority.normal}) async { + Future> syncAll( + {SyncPriority priority = SyncPriority.normal}) async { final operationIds = []; // Sync local items to Immich @@ -239,7 +243,8 @@ class SyncEngine { /// Processes the sync queue. Future _processQueue() async { - if (_currentOperation != null || _isDisposed) return; // Already processing or disposed + if (_currentOperation != null || _isDisposed) + return; // Already processing or disposed // Sort queue by priority (high first) _operationQueue.sort((a, b) { @@ -250,7 +255,8 @@ class SyncEngine { }); // Process pending operations - while (!_isDisposed && _operationQueue.any((op) => op.status == SyncStatus.pending)) { + while (!_isDisposed && + _operationQueue.any((op) => op.status == SyncStatus.pending)) { final operation = _operationQueue.firstWhere( (op) => op.status == SyncStatus.pending, ); @@ -264,7 +270,7 @@ class SyncEngine { operation.markSuccess(); } catch (e) { operation.markFailed(e.toString()); - + // Retry if possible if (operation.canRetry() && !_isDisposed) { await Future.delayed(Duration(seconds: operation.retryCount)); @@ -374,10 +380,10 @@ class SyncEngine { } /// Resolves a conflict between local and remote data. - /// + /// /// [localItem] - Local item data. /// [remoteItem] - Remote item data. - /// + /// /// Returns the resolved item data. Map resolveConflict( Map localItem, @@ -421,4 +427,3 @@ class SyncEngine { } } } - diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/test/data/firebase/firebase_service_test.dart b/test/data/firebase/firebase_service_test.dart index e2518a3..b279ad5 100644 --- a/test/data/firebase/firebase_service_test.dart +++ b/test/data/firebase/firebase_service_test.dart @@ -1,11 +1,9 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:app_boilerplate/data/firebase/firebase_service.dart'; import 'package:app_boilerplate/data/firebase/models/firebase_config.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; -import 'package:app_boilerplate/data/local/models/item.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:path/path.dart' as path; @@ -307,4 +305,3 @@ void main() { }); }); } - diff --git a/test/data/immich/immich_service_test.dart b/test/data/immich/immich_service_test.dart index 5dc998b..95fe3af 100644 --- a/test/data/immich/immich_service_test.dart +++ b/test/data/immich/immich_service_test.dart @@ -1,10 +1,8 @@ import 'dart:io'; -import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:dio/dio.dart'; import 'package:app_boilerplate/data/immich/immich_service.dart'; import 'package:app_boilerplate/data/immich/models/immich_asset.dart'; -import 'package:app_boilerplate/data/immich/models/upload_response.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/models/item.dart'; import 'package:path/path.dart' as path; @@ -318,7 +316,10 @@ void main() { if (path == '/api/search/metadata') { return Response( statusCode: 500, - data: {'error': 'Internal server error', 'message': 'Internal server error'}, + data: { + 'error': 'Internal server error', + 'message': 'Internal server error' + }, requestOptions: RequestOptions(path: path), ); } @@ -445,7 +446,8 @@ void main() { // Assert expect(cached.length, equals(2)); - expect(cached.map((a) => a.id).toList(), containsAll(['asset-1', 'asset-2'])); + expect(cached.map((a) => a.id).toList(), + containsAll(['asset-1', 'asset-2'])); }); }); } diff --git a/test/data/nostr/nostr_service_test.dart b/test/data/nostr/nostr_service_test.dart index 7d8cd3b..b8e8c8f 100644 --- a/test/data/nostr/nostr_service_test.dart +++ b/test/data/nostr/nostr_service_test.dart @@ -2,9 +2,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart'; import 'package:app_boilerplate/data/nostr/models/nostr_event.dart'; -import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart'; -import 'dart:async'; -import 'dart:convert'; void main() { group('NostrService - Keypair Generation', () { @@ -303,4 +300,3 @@ void main() { }); }); } - diff --git a/test/data/session/session_service_test.dart b/test/data/session/session_service_test.dart index f26d40b..6f06c15 100644 --- a/test/data/session/session_service_test.dart +++ b/test/data/session/session_service_test.dart @@ -3,10 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:app_boilerplate/data/session/session_service.dart'; -import 'package:app_boilerplate/data/session/models/user.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:app_boilerplate/data/local/models/item.dart'; -import 'package:app_boilerplate/data/sync/sync_engine.dart'; void main() { // Initialize Flutter bindings and sqflite for testing @@ -48,7 +46,7 @@ void main() { } catch (_) { // Ignore cleanup errors } - + // Clean up temporary directory if (await tempDir.exists()) { await tempDir.delete(recursive: true); @@ -123,14 +121,14 @@ void main() { test('logout - clears cache when clearCache is true', () async { // Arrange await sessionService.login(id: 'user1', username: 'user1'); - + // Add some data to storage final item = Item( id: 'item1', data: {'test': 'data'}, ); await localStorage.insertItem(item); - + // Verify data exists final itemsBefore = await localStorage.getAllItems(); expect(itemsBefore.length, equals(1)); @@ -148,7 +146,7 @@ void main() { test('logout - preserves cache when clearCache is false', () async { // Arrange await sessionService.login(id: 'user1', username: 'user1'); - + // Add some data to storage final item = Item( id: 'item1', @@ -191,10 +189,11 @@ void main() { expect(newUser.id, equals('user2')); }); - test('switchSession - clears previous user data when clearCache is true', () async { + test('switchSession - clears previous user data when clearCache is true', + () async { // Arrange await sessionService.login(id: 'user1', username: 'user1'); - + // Add data for user1 final item1 = Item( id: 'item1', @@ -216,10 +215,12 @@ void main() { expect(items.length, equals(0)); }); - test('switchSession - preserves previous user data when clearCache is false', () async { + test( + 'switchSession - preserves previous user data when clearCache is false', + () async { // Arrange await sessionService.login(id: 'user1', username: 'user1'); - + // Add data for user1 final item1 = Item( id: 'item1', @@ -304,12 +305,12 @@ void main() { test('cache clearing - clears all items on logout', () async { // Arrange await sessionService.login(id: 'user1', username: 'user1'); - + // Add multiple items await localStorage.insertItem(Item(id: 'item1', data: {'test': '1'})); await localStorage.insertItem(Item(id: 'item2', data: {'test': '2'})); await localStorage.insertItem(Item(id: 'item3', data: {'test': '3'})); - + final itemsBefore = await localStorage.getAllItems(); expect(itemsBefore.length, equals(3)); @@ -323,4 +324,3 @@ void main() { }); }); } - diff --git a/test/data/sync/sync_engine_test.dart b/test/data/sync/sync_engine_test.dart index 4ba9afe..f70a5c1 100644 --- a/test/data/sync/sync_engine_test.dart +++ b/test/data/sync/sync_engine_test.dart @@ -11,7 +11,6 @@ import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart'; import 'package:path/path.dart' as path; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:dio/dio.dart'; -import 'dart:convert'; void main() { // Initialize Flutter bindings and sqflite for testing @@ -511,4 +510,3 @@ Dio _createMockDio({ return dio; } - diff --git a/test/ui/relay_management/relay_management_controller_test.dart b/test/ui/relay_management/relay_management_controller_test.dart index 5989310..969b552 100644 --- a/test/ui/relay_management/relay_management_controller_test.dart +++ b/test/ui/relay_management/relay_management_controller_test.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart'; -import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:path/path.dart' as path; @@ -141,7 +140,7 @@ void main() { // Swallow any unhandled errors - connection failures are expected }, ); - + // Verify the health check completed expect(controller.isCheckingHealth, isFalse); // Relay should still be in the list (even if disconnected) @@ -157,10 +156,10 @@ void main() { final result = await controllerWithoutSync.triggerManualSync(); expect(result, isFalse); expect(controllerWithoutSync.error, isNotNull); - expect(controllerWithoutSync.error, contains('Sync engine not configured')); + expect( + controllerWithoutSync.error, contains('Sync engine not configured')); controllerWithoutSync.dispose(); }); }); } - diff --git a/test/ui/relay_management/relay_management_screen_test.dart b/test/ui/relay_management/relay_management_screen_test.dart index 5ddd7dc..a0b3f69 100644 --- a/test/ui/relay_management/relay_management_screen_test.dart +++ b/test/ui/relay_management/relay_management_screen_test.dart @@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_screen.dart'; import 'package:app_boilerplate/ui/relay_management/relay_management_controller.dart'; import 'package:app_boilerplate/data/nostr/nostr_service.dart'; -import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart'; import 'package:app_boilerplate/data/sync/sync_engine.dart'; import 'package:app_boilerplate/data/local/local_storage_service.dart'; import 'package:path/path.dart' as path; @@ -72,7 +71,8 @@ void main() { } group('RelayManagementScreen', () { - testWidgets('displays empty state when no relays', (WidgetTester tester) async { + testWidgets('displays empty state when no relays', + (WidgetTester tester) async { await tester.pumpWidget(createTestWidget()); expect(find.text('No relays configured'), findsOneWidget); @@ -95,7 +95,8 @@ void main() { expect(find.text('Disconnected'), findsNWidgets(2)); }); - testWidgets('adds relay when Add button is pressed', (WidgetTester tester) async { + testWidgets('adds relay when Add button is pressed', + (WidgetTester tester) async { await tester.pumpWidget(createTestWidget()); // Find and enter relay URL @@ -131,7 +132,8 @@ void main() { expect(find.byIcon(Icons.error), findsOneWidget); }); - testWidgets('removes relay when delete button is pressed', (WidgetTester tester) async { + testWidgets('removes relay when delete button is pressed', + (WidgetTester tester) async { controller.addRelay('wss://relay.example.com'); await tester.pumpWidget(createTestWidget()); await tester.pump(); @@ -159,14 +161,16 @@ void main() { expect(find.byIcon(Icons.health_and_safety), findsOneWidget); }); - testWidgets('displays manual sync button when sync engine is configured', (WidgetTester tester) async { + testWidgets('displays manual sync button when sync engine is configured', + (WidgetTester tester) async { await tester.pumpWidget(createTestWidget()); expect(find.text('Manual Sync'), findsOneWidget); expect(find.byIcon(Icons.sync), findsOneWidget); }); - testWidgets('shows loading state during health check', (WidgetTester tester) async { + testWidgets('shows loading state during health check', + (WidgetTester tester) async { controller.addRelay('wss://relay.example.com'); await tester.pumpWidget(createTestWidget()); await tester.pump(); @@ -178,12 +182,13 @@ void main() { // Check for loading indicator (may be brief) expect(find.byType(CircularProgressIndicator), findsWidgets); - + // Wait for health check to complete await tester.pumpAndSettle(); }); - testWidgets('shows error message when present', (WidgetTester tester) async { + testWidgets('shows error message when present', + (WidgetTester tester) async { await tester.pumpWidget(createTestWidget()); // Trigger an error by adding invalid URL @@ -198,7 +203,8 @@ void main() { expect(find.textContaining('Invalid relay URL'), findsOneWidget); }); - testWidgets('dismisses error when close button is pressed', (WidgetTester tester) async { + testWidgets('dismisses error when close button is pressed', + (WidgetTester tester) async { await tester.pumpWidget(createTestWidget()); // Trigger an error @@ -234,4 +240,3 @@ void main() { }); }); } -