some fixes
This commit is contained in:
@@ -7,7 +7,6 @@ import 'package:firebase_messaging/firebase_messaging.dart';
|
|||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import '../local/local_storage_service.dart';
|
import '../local/local_storage_service.dart';
|
||||||
import '../local/models/item.dart';
|
import '../local/models/item.dart';
|
||||||
import '../session/models/user.dart';
|
|
||||||
import 'models/firebase_config.dart';
|
import 'models/firebase_config.dart';
|
||||||
|
|
||||||
/// Exception thrown when Firebase operations fail.
|
/// 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).
|
/// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics).
|
||||||
///
|
///
|
||||||
/// This service provides:
|
/// This service provides:
|
||||||
/// - Cloud Firestore for optional metadata sync and backup
|
/// - Cloud Firestore for optional metadata sync and backup
|
||||||
/// - Firebase Storage for optional media storage
|
/// - Firebase Storage for optional media storage
|
||||||
/// - Firebase Authentication for user login/logout
|
/// - Firebase Authentication for user login/logout
|
||||||
/// - Firebase Cloud Messaging for push notifications
|
/// - Firebase Cloud Messaging for push notifications
|
||||||
/// - Firebase Analytics for optional analytics
|
/// - Firebase Analytics for optional analytics
|
||||||
///
|
///
|
||||||
/// The service is modular and optional - can be enabled/disabled without affecting other modules.
|
/// The service is modular and optional - can be enabled/disabled without affecting other modules.
|
||||||
/// When disabled, all methods return safely without throwing errors.
|
/// When disabled, all methods return safely without throwing errors.
|
||||||
///
|
///
|
||||||
/// The service maintains offline-first behavior by syncing with local storage
|
/// The service maintains offline-first behavior by syncing with local storage
|
||||||
/// and only using Firebase as an optional cloud backup/sync layer.
|
/// and only using Firebase as an optional cloud backup/sync layer.
|
||||||
class FirebaseService {
|
class FirebaseService {
|
||||||
@@ -65,7 +64,7 @@ class FirebaseService {
|
|||||||
firebase_auth.User? _firebaseUser;
|
firebase_auth.User? _firebaseUser;
|
||||||
|
|
||||||
/// Creates a [FirebaseService] instance.
|
/// Creates a [FirebaseService] instance.
|
||||||
///
|
///
|
||||||
/// [config] - Firebase configuration (determines which services are enabled).
|
/// [config] - Firebase configuration (determines which services are enabled).
|
||||||
/// [localStorage] - Local storage service for offline-first behavior.
|
/// [localStorage] - Local storage service for offline-first behavior.
|
||||||
FirebaseService({
|
FirebaseService({
|
||||||
@@ -83,10 +82,10 @@ class FirebaseService {
|
|||||||
bool get isLoggedIn => _auth != null && _firebaseUser != null;
|
bool get isLoggedIn => _auth != null && _firebaseUser != null;
|
||||||
|
|
||||||
/// Initializes Firebase services based on configuration.
|
/// Initializes Firebase services based on configuration.
|
||||||
///
|
///
|
||||||
/// Must be called before using any Firebase services.
|
/// Must be called before using any Firebase services.
|
||||||
/// If Firebase is disabled, this method does nothing.
|
/// If Firebase is disabled, this method does nothing.
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if initialization fails.
|
/// Throws [FirebaseException] if initialization fails.
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
@@ -141,12 +140,12 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Logs in a user with email and password.
|
/// Logs in a user with email and password.
|
||||||
///
|
///
|
||||||
/// [email] - User email address.
|
/// [email] - User email address.
|
||||||
/// [password] - User password.
|
/// [password] - User password.
|
||||||
///
|
///
|
||||||
/// Returns the Firebase Auth user.
|
/// Returns the Firebase Auth user.
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if auth is disabled or login fails.
|
/// Throws [FirebaseException] if auth is disabled or login fails.
|
||||||
Future<firebase_auth.User> loginWithEmailPassword({
|
Future<firebase_auth.User> loginWithEmailPassword({
|
||||||
required String email,
|
required String email,
|
||||||
@@ -157,7 +156,8 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_initialized || _auth == null) {
|
if (!_initialized || _auth == null) {
|
||||||
throw FirebaseException('Firebase not initialized. Call initialize() first.');
|
throw FirebaseException(
|
||||||
|
'Firebase not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -173,7 +173,7 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Logs out the current user.
|
/// Logs out the current user.
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if auth is disabled or logout fails.
|
/// Throws [FirebaseException] if auth is disabled or logout fails.
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
if (!config.enabled || !config.authEnabled) {
|
if (!config.enabled || !config.authEnabled) {
|
||||||
@@ -181,7 +181,8 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_initialized || _auth == null) {
|
if (!_initialized || _auth == null) {
|
||||||
throw FirebaseException('Firebase not initialized. Call initialize() first.');
|
throw FirebaseException(
|
||||||
|
'Firebase not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,9 +194,9 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs local items to Firestore (cloud backup).
|
/// Syncs local items to Firestore (cloud backup).
|
||||||
///
|
///
|
||||||
/// [userId] - User ID to associate items with (for multi-user support).
|
/// [userId] - User ID to associate items with (for multi-user support).
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
||||||
Future<void> syncItemsToFirestore(String userId) async {
|
Future<void> syncItemsToFirestore(String userId) async {
|
||||||
if (!config.enabled || !config.firestoreEnabled) {
|
if (!config.enabled || !config.firestoreEnabled) {
|
||||||
@@ -203,7 +204,8 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_initialized || _firestore == null) {
|
if (!_initialized || _firestore == null) {
|
||||||
throw FirebaseException('Firestore not initialized. Call initialize() first.');
|
throw FirebaseException(
|
||||||
|
'Firestore not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -212,7 +214,8 @@ class FirebaseService {
|
|||||||
|
|
||||||
// Batch write to Firestore
|
// Batch write to Firestore
|
||||||
final batch = _firestore!.batch();
|
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) {
|
for (final item in items) {
|
||||||
final docRef = collection.doc(item.id);
|
final docRef = collection.doc(item.id);
|
||||||
@@ -231,9 +234,9 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs items from Firestore to local storage.
|
/// Syncs items from Firestore to local storage.
|
||||||
///
|
///
|
||||||
/// [userId] - User ID to fetch items for.
|
/// [userId] - User ID to fetch items for.
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
/// Throws [FirebaseException] if Firestore is disabled or sync fails.
|
||||||
Future<void> syncItemsFromFirestore(String userId) async {
|
Future<void> syncItemsFromFirestore(String userId) async {
|
||||||
if (!config.enabled || !config.firestoreEnabled) {
|
if (!config.enabled || !config.firestoreEnabled) {
|
||||||
@@ -241,7 +244,8 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_initialized || _firestore == null) {
|
if (!_initialized || _firestore == null) {
|
||||||
throw FirebaseException('Firestore not initialized. Call initialize() first.');
|
throw FirebaseException(
|
||||||
|
'Firestore not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -272,12 +276,12 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Uploads a file to Firebase Storage.
|
/// Uploads a file to Firebase Storage.
|
||||||
///
|
///
|
||||||
/// [file] - File to upload.
|
/// [file] - File to upload.
|
||||||
/// [path] - Storage path (e.g., 'users/userId/media/image.jpg').
|
/// [path] - Storage path (e.g., 'users/userId/media/image.jpg').
|
||||||
///
|
///
|
||||||
/// Returns the download URL.
|
/// Returns the download URL.
|
||||||
///
|
///
|
||||||
/// Throws [FirebaseException] if Storage is disabled or upload fails.
|
/// Throws [FirebaseException] if Storage is disabled or upload fails.
|
||||||
Future<String> uploadFile(File file, String path) async {
|
Future<String> uploadFile(File file, String path) async {
|
||||||
if (!config.enabled || !config.storageEnabled) {
|
if (!config.enabled || !config.storageEnabled) {
|
||||||
@@ -285,7 +289,8 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_initialized || _storage == null) {
|
if (!_initialized || _storage == null) {
|
||||||
throw FirebaseException('Firebase Storage not initialized. Call initialize() first.');
|
throw FirebaseException(
|
||||||
|
'Firebase Storage not initialized. Call initialize() first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -298,7 +303,7 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the FCM token for push notifications.
|
/// Gets the FCM token for push notifications.
|
||||||
///
|
///
|
||||||
/// Returns the FCM token, or null if messaging is disabled.
|
/// Returns the FCM token, or null if messaging is disabled.
|
||||||
Future<String?> getFcmToken() async {
|
Future<String?> getFcmToken() async {
|
||||||
if (!config.enabled || !config.messagingEnabled || _messaging == null) {
|
if (!config.enabled || !config.messagingEnabled || _messaging == null) {
|
||||||
@@ -313,12 +318,13 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Logs an event to Firebase Analytics.
|
/// Logs an event to Firebase Analytics.
|
||||||
///
|
///
|
||||||
/// [eventName] - Name of the event.
|
/// [eventName] - Name of the event.
|
||||||
/// [parameters] - Optional event parameters.
|
/// [parameters] - Optional event parameters.
|
||||||
///
|
///
|
||||||
/// Does nothing if Analytics is disabled.
|
/// Does nothing if Analytics is disabled.
|
||||||
Future<void> logEvent(String eventName, {Map<String, dynamic>? parameters}) async {
|
Future<void> logEvent(String eventName,
|
||||||
|
{Map<String, dynamic>? parameters}) async {
|
||||||
if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
|
if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -327,7 +333,8 @@ class FirebaseService {
|
|||||||
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
|
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
|
||||||
Map<String, Object>? analyticsParams;
|
Map<String, Object>? analyticsParams;
|
||||||
if (parameters != null) {
|
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(
|
await _analytics!.logEvent(
|
||||||
@@ -340,7 +347,7 @@ class FirebaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Disposes of Firebase resources.
|
/// Disposes of Firebase resources.
|
||||||
///
|
///
|
||||||
/// Should be called when the service is no longer needed.
|
/// Should be called when the service is no longer needed.
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if (_auth != null) {
|
if (_auth != null) {
|
||||||
@@ -350,4 +357,3 @@ class FirebaseService {
|
|||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,6 @@ class ImmichService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final assetsJson = assetsData['items'] as List<dynamic>;
|
final assetsJson = assetsData['items'] as List<dynamic>;
|
||||||
final total = assetsData['total'] as int? ?? 0;
|
|
||||||
final count = assetsData['count'] as int? ?? 0;
|
|
||||||
|
|
||||||
final List<ImmichAsset> assets = assetsJson
|
final List<ImmichAsset> assets = assetsJson
|
||||||
.map((json) => ImmichAsset.fromJson(json as Map<String, dynamic>))
|
.map((json) => ImmichAsset.fromJson(json as Map<String, dynamic>))
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import '../local/local_storage_service.dart';
|
import '../local/local_storage_service.dart';
|
||||||
import '../local/models/item.dart';
|
|
||||||
import '../immich/immich_service.dart';
|
import '../immich/immich_service.dart';
|
||||||
import '../immich/models/immich_asset.dart';
|
|
||||||
import '../nostr/nostr_service.dart';
|
import '../nostr/nostr_service.dart';
|
||||||
import '../nostr/models/nostr_event.dart';
|
|
||||||
import '../nostr/models/nostr_keypair.dart';
|
import '../nostr/models/nostr_keypair.dart';
|
||||||
import 'models/sync_status.dart';
|
import 'models/sync_status.dart';
|
||||||
import 'models/sync_operation.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.
|
/// Engine for coordinating data synchronization between local storage, Immich, and Nostr.
|
||||||
///
|
///
|
||||||
/// This service provides:
|
/// This service provides:
|
||||||
/// - Bidirectional sync between local storage, Immich, and Nostr
|
/// - Bidirectional sync between local storage, Immich, and Nostr
|
||||||
/// - Conflict resolution strategies
|
/// - Conflict resolution strategies
|
||||||
/// - Offline queue for operations when network is unavailable
|
/// - Offline queue for operations when network is unavailable
|
||||||
/// - Automatic retry with exponential backoff
|
/// - Automatic retry with exponential backoff
|
||||||
///
|
///
|
||||||
/// The service is modular and UI-independent, designed for offline-first behavior.
|
/// The service is modular and UI-independent, designed for offline-first behavior.
|
||||||
class SyncEngine {
|
class SyncEngine {
|
||||||
/// Local storage service.
|
/// Local storage service.
|
||||||
@@ -50,7 +47,8 @@ class SyncEngine {
|
|||||||
SyncOperation? _currentOperation;
|
SyncOperation? _currentOperation;
|
||||||
|
|
||||||
/// Stream controller for sync status updates.
|
/// Stream controller for sync status updates.
|
||||||
final StreamController<SyncOperation> _statusController = StreamController<SyncOperation>.broadcast();
|
final StreamController<SyncOperation> _statusController =
|
||||||
|
StreamController<SyncOperation>.broadcast();
|
||||||
|
|
||||||
/// Whether the engine has been disposed.
|
/// Whether the engine has been disposed.
|
||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
@@ -62,7 +60,7 @@ class SyncEngine {
|
|||||||
final int maxQueueSize;
|
final int maxQueueSize;
|
||||||
|
|
||||||
/// Creates a [SyncEngine] instance.
|
/// Creates a [SyncEngine] instance.
|
||||||
///
|
///
|
||||||
/// [localStorage] - Local storage service (required).
|
/// [localStorage] - Local storage service (required).
|
||||||
/// [immichService] - Immich service (optional).
|
/// [immichService] - Immich service (optional).
|
||||||
/// [nostrService] - Nostr service (optional).
|
/// [nostrService] - Nostr service (optional).
|
||||||
@@ -97,7 +95,9 @@ class SyncEngine {
|
|||||||
|
|
||||||
/// Gets the current queue of pending operations.
|
/// Gets the current queue of pending operations.
|
||||||
List<SyncOperation> getPendingOperations() {
|
List<SyncOperation> 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).
|
/// Gets all operations (pending, in-progress, completed, failed).
|
||||||
@@ -106,15 +106,15 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Queues a sync operation.
|
/// Queues a sync operation.
|
||||||
///
|
///
|
||||||
/// [operation] - The sync operation to queue.
|
/// [operation] - The sync operation to queue.
|
||||||
///
|
///
|
||||||
/// Throws [SyncException] if queue is full.
|
/// Throws [SyncException] if queue is full.
|
||||||
void queueOperation(SyncOperation operation) {
|
void queueOperation(SyncOperation operation) {
|
||||||
if (_isDisposed) {
|
if (_isDisposed) {
|
||||||
throw SyncException('SyncEngine has been disposed');
|
throw SyncException('SyncEngine has been disposed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_operationQueue.length >= maxQueueSize) {
|
if (_operationQueue.length >= maxQueueSize) {
|
||||||
throw SyncException('Sync queue is full (max: $maxQueueSize)');
|
throw SyncException('Sync queue is full (max: $maxQueueSize)');
|
||||||
}
|
}
|
||||||
@@ -129,12 +129,13 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs an item from local storage to Immich.
|
/// Syncs an item from local storage to Immich.
|
||||||
///
|
///
|
||||||
/// [itemId] - The ID of the item to sync.
|
/// [itemId] - The ID of the item to sync.
|
||||||
/// [priority] - Priority of the sync operation.
|
/// [priority] - Priority of the sync operation.
|
||||||
///
|
///
|
||||||
/// Returns the sync operation ID.
|
/// Returns the sync operation ID.
|
||||||
Future<String> syncToImmich(String itemId, {SyncPriority priority = SyncPriority.normal}) async {
|
Future<String> syncToImmich(String itemId,
|
||||||
|
{SyncPriority priority = SyncPriority.normal}) async {
|
||||||
if (_immichService == null) {
|
if (_immichService == null) {
|
||||||
throw SyncException('Immich service not configured');
|
throw SyncException('Immich service not configured');
|
||||||
}
|
}
|
||||||
@@ -153,12 +154,13 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs metadata from Immich to local storage.
|
/// Syncs metadata from Immich to local storage.
|
||||||
///
|
///
|
||||||
/// [assetId] - The Immich asset ID to sync.
|
/// [assetId] - The Immich asset ID to sync.
|
||||||
/// [priority] - Priority of the sync operation.
|
/// [priority] - Priority of the sync operation.
|
||||||
///
|
///
|
||||||
/// Returns the sync operation ID.
|
/// Returns the sync operation ID.
|
||||||
Future<String> syncFromImmich(String assetId, {SyncPriority priority = SyncPriority.normal}) async {
|
Future<String> syncFromImmich(String assetId,
|
||||||
|
{SyncPriority priority = SyncPriority.normal}) async {
|
||||||
if (_immichService == null) {
|
if (_immichService == null) {
|
||||||
throw SyncException('Immich service not configured');
|
throw SyncException('Immich service not configured');
|
||||||
}
|
}
|
||||||
@@ -177,12 +179,13 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs metadata to Nostr.
|
/// Syncs metadata to Nostr.
|
||||||
///
|
///
|
||||||
/// [itemId] - The ID of the item to sync.
|
/// [itemId] - The ID of the item to sync.
|
||||||
/// [priority] - Priority of the sync operation.
|
/// [priority] - Priority of the sync operation.
|
||||||
///
|
///
|
||||||
/// Returns the sync operation ID.
|
/// Returns the sync operation ID.
|
||||||
Future<String> syncToNostr(String itemId, {SyncPriority priority = SyncPriority.normal}) async {
|
Future<String> syncToNostr(String itemId,
|
||||||
|
{SyncPriority priority = SyncPriority.normal}) async {
|
||||||
if (_nostrService == null) {
|
if (_nostrService == null) {
|
||||||
throw SyncException('Nostr service not configured');
|
throw SyncException('Nostr service not configured');
|
||||||
}
|
}
|
||||||
@@ -205,11 +208,12 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Performs a full sync: syncs all items between configured services.
|
/// Performs a full sync: syncs all items between configured services.
|
||||||
///
|
///
|
||||||
/// [priority] - Priority of sync operations.
|
/// [priority] - Priority of sync operations.
|
||||||
///
|
///
|
||||||
/// Returns a list of operation IDs.
|
/// Returns a list of operation IDs.
|
||||||
Future<List<String>> syncAll({SyncPriority priority = SyncPriority.normal}) async {
|
Future<List<String>> syncAll(
|
||||||
|
{SyncPriority priority = SyncPriority.normal}) async {
|
||||||
final operationIds = <String>[];
|
final operationIds = <String>[];
|
||||||
|
|
||||||
// Sync local items to Immich
|
// Sync local items to Immich
|
||||||
@@ -239,7 +243,8 @@ class SyncEngine {
|
|||||||
|
|
||||||
/// Processes the sync queue.
|
/// Processes the sync queue.
|
||||||
Future<void> _processQueue() async {
|
Future<void> _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)
|
// Sort queue by priority (high first)
|
||||||
_operationQueue.sort((a, b) {
|
_operationQueue.sort((a, b) {
|
||||||
@@ -250,7 +255,8 @@ class SyncEngine {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Process pending operations
|
// 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(
|
final operation = _operationQueue.firstWhere(
|
||||||
(op) => op.status == SyncStatus.pending,
|
(op) => op.status == SyncStatus.pending,
|
||||||
);
|
);
|
||||||
@@ -264,7 +270,7 @@ class SyncEngine {
|
|||||||
operation.markSuccess();
|
operation.markSuccess();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
operation.markFailed(e.toString());
|
operation.markFailed(e.toString());
|
||||||
|
|
||||||
// Retry if possible
|
// Retry if possible
|
||||||
if (operation.canRetry() && !_isDisposed) {
|
if (operation.canRetry() && !_isDisposed) {
|
||||||
await Future.delayed(Duration(seconds: operation.retryCount));
|
await Future.delayed(Duration(seconds: operation.retryCount));
|
||||||
@@ -374,10 +380,10 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves a conflict between local and remote data.
|
/// Resolves a conflict between local and remote data.
|
||||||
///
|
///
|
||||||
/// [localItem] - Local item data.
|
/// [localItem] - Local item data.
|
||||||
/// [remoteItem] - Remote item data.
|
/// [remoteItem] - Remote item data.
|
||||||
///
|
///
|
||||||
/// Returns the resolved item data.
|
/// Returns the resolved item data.
|
||||||
Map<String, dynamic> resolveConflict(
|
Map<String, dynamic> resolveConflict(
|
||||||
Map<String, dynamic> localItem,
|
Map<String, dynamic> localItem,
|
||||||
@@ -421,4 +427,3 @@ class SyncEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mockito/annotations.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/firebase_service.dart';
|
||||||
import 'package:app_boilerplate/data/firebase/models/firebase_config.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/local_storage_service.dart';
|
||||||
import 'package:app_boilerplate/data/local/models/item.dart';
|
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
@@ -307,4 +305,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:app_boilerplate/data/immich/immich_service.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/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/local_storage_service.dart';
|
||||||
import 'package:app_boilerplate/data/local/models/item.dart';
|
import 'package:app_boilerplate/data/local/models/item.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@@ -318,7 +316,10 @@ void main() {
|
|||||||
if (path == '/api/search/metadata') {
|
if (path == '/api/search/metadata') {
|
||||||
return Response(
|
return Response(
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
data: {'error': 'Internal server error', 'message': 'Internal server error'},
|
data: {
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'Internal server error'
|
||||||
|
},
|
||||||
requestOptions: RequestOptions(path: path),
|
requestOptions: RequestOptions(path: path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -445,7 +446,8 @@ void main() {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(cached.length, equals(2));
|
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']));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/nostr_service.dart';
|
||||||
import 'package:app_boilerplate/data/nostr/models/nostr_keypair.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_event.dart';
|
||||||
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('NostrService - Keypair Generation', () {
|
group('NostrService - Keypair Generation', () {
|
||||||
@@ -303,4 +300,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'package:app_boilerplate/data/session/session_service.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/local_storage_service.dart';
|
||||||
import 'package:app_boilerplate/data/local/models/item.dart';
|
import 'package:app_boilerplate/data/local/models/item.dart';
|
||||||
import 'package:app_boilerplate/data/sync/sync_engine.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Initialize Flutter bindings and sqflite for testing
|
// Initialize Flutter bindings and sqflite for testing
|
||||||
@@ -48,7 +46,7 @@ void main() {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temporary directory
|
// Clean up temporary directory
|
||||||
if (await tempDir.exists()) {
|
if (await tempDir.exists()) {
|
||||||
await tempDir.delete(recursive: true);
|
await tempDir.delete(recursive: true);
|
||||||
@@ -123,14 +121,14 @@ void main() {
|
|||||||
test('logout - clears cache when clearCache is true', () async {
|
test('logout - clears cache when clearCache is true', () async {
|
||||||
// Arrange
|
// Arrange
|
||||||
await sessionService.login(id: 'user1', username: 'user1');
|
await sessionService.login(id: 'user1', username: 'user1');
|
||||||
|
|
||||||
// Add some data to storage
|
// Add some data to storage
|
||||||
final item = Item(
|
final item = Item(
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
data: {'test': 'data'},
|
data: {'test': 'data'},
|
||||||
);
|
);
|
||||||
await localStorage.insertItem(item);
|
await localStorage.insertItem(item);
|
||||||
|
|
||||||
// Verify data exists
|
// Verify data exists
|
||||||
final itemsBefore = await localStorage.getAllItems();
|
final itemsBefore = await localStorage.getAllItems();
|
||||||
expect(itemsBefore.length, equals(1));
|
expect(itemsBefore.length, equals(1));
|
||||||
@@ -148,7 +146,7 @@ void main() {
|
|||||||
test('logout - preserves cache when clearCache is false', () async {
|
test('logout - preserves cache when clearCache is false', () async {
|
||||||
// Arrange
|
// Arrange
|
||||||
await sessionService.login(id: 'user1', username: 'user1');
|
await sessionService.login(id: 'user1', username: 'user1');
|
||||||
|
|
||||||
// Add some data to storage
|
// Add some data to storage
|
||||||
final item = Item(
|
final item = Item(
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
@@ -191,10 +189,11 @@ void main() {
|
|||||||
expect(newUser.id, equals('user2'));
|
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
|
// Arrange
|
||||||
await sessionService.login(id: 'user1', username: 'user1');
|
await sessionService.login(id: 'user1', username: 'user1');
|
||||||
|
|
||||||
// Add data for user1
|
// Add data for user1
|
||||||
final item1 = Item(
|
final item1 = Item(
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
@@ -216,10 +215,12 @@ void main() {
|
|||||||
expect(items.length, equals(0));
|
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
|
// Arrange
|
||||||
await sessionService.login(id: 'user1', username: 'user1');
|
await sessionService.login(id: 'user1', username: 'user1');
|
||||||
|
|
||||||
// Add data for user1
|
// Add data for user1
|
||||||
final item1 = Item(
|
final item1 = Item(
|
||||||
id: 'item1',
|
id: 'item1',
|
||||||
@@ -304,12 +305,12 @@ void main() {
|
|||||||
test('cache clearing - clears all items on logout', () async {
|
test('cache clearing - clears all items on logout', () async {
|
||||||
// Arrange
|
// Arrange
|
||||||
await sessionService.login(id: 'user1', username: 'user1');
|
await sessionService.login(id: 'user1', username: 'user1');
|
||||||
|
|
||||||
// Add multiple items
|
// Add multiple items
|
||||||
await localStorage.insertItem(Item(id: 'item1', data: {'test': '1'}));
|
await localStorage.insertItem(Item(id: 'item1', data: {'test': '1'}));
|
||||||
await localStorage.insertItem(Item(id: 'item2', data: {'test': '2'}));
|
await localStorage.insertItem(Item(id: 'item2', data: {'test': '2'}));
|
||||||
await localStorage.insertItem(Item(id: 'item3', data: {'test': '3'}));
|
await localStorage.insertItem(Item(id: 'item3', data: {'test': '3'}));
|
||||||
|
|
||||||
final itemsBefore = await localStorage.getAllItems();
|
final itemsBefore = await localStorage.getAllItems();
|
||||||
expect(itemsBefore.length, equals(3));
|
expect(itemsBefore.length, equals(3));
|
||||||
|
|
||||||
@@ -323,4 +324,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart';
|
|||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// Initialize Flutter bindings and sqflite for testing
|
// Initialize Flutter bindings and sqflite for testing
|
||||||
@@ -511,4 +510,3 @@ Dio _createMockDio({
|
|||||||
|
|
||||||
return dio;
|
return dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:app_boilerplate/ui/relay_management/relay_management_controller.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/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/sync/sync_engine.dart';
|
||||||
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@@ -141,7 +140,7 @@ void main() {
|
|||||||
// Swallow any unhandled errors - connection failures are expected
|
// Swallow any unhandled errors - connection failures are expected
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the health check completed
|
// Verify the health check completed
|
||||||
expect(controller.isCheckingHealth, isFalse);
|
expect(controller.isCheckingHealth, isFalse);
|
||||||
// Relay should still be in the list (even if disconnected)
|
// Relay should still be in the list (even if disconnected)
|
||||||
@@ -157,10 +156,10 @@ void main() {
|
|||||||
final result = await controllerWithoutSync.triggerManualSync();
|
final result = await controllerWithoutSync.triggerManualSync();
|
||||||
expect(result, isFalse);
|
expect(result, isFalse);
|
||||||
expect(controllerWithoutSync.error, isNotNull);
|
expect(controllerWithoutSync.error, isNotNull);
|
||||||
expect(controllerWithoutSync.error, contains('Sync engine not configured'));
|
expect(
|
||||||
|
controllerWithoutSync.error, contains('Sync engine not configured'));
|
||||||
|
|
||||||
controllerWithoutSync.dispose();
|
controllerWithoutSync.dispose();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_screen.dart';
|
||||||
import 'package:app_boilerplate/ui/relay_management/relay_management_controller.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/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/sync/sync_engine.dart';
|
||||||
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
@@ -72,7 +71,8 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group('RelayManagementScreen', () {
|
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());
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
|
||||||
expect(find.text('No relays configured'), findsOneWidget);
|
expect(find.text('No relays configured'), findsOneWidget);
|
||||||
@@ -95,7 +95,8 @@ void main() {
|
|||||||
expect(find.text('Disconnected'), findsNWidgets(2));
|
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());
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
|
||||||
// Find and enter relay URL
|
// Find and enter relay URL
|
||||||
@@ -131,7 +132,8 @@ void main() {
|
|||||||
expect(find.byIcon(Icons.error), findsOneWidget);
|
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');
|
controller.addRelay('wss://relay.example.com');
|
||||||
await tester.pumpWidget(createTestWidget());
|
await tester.pumpWidget(createTestWidget());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@@ -159,14 +161,16 @@ void main() {
|
|||||||
expect(find.byIcon(Icons.health_and_safety), findsOneWidget);
|
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());
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
|
||||||
expect(find.text('Manual Sync'), findsOneWidget);
|
expect(find.text('Manual Sync'), findsOneWidget);
|
||||||
expect(find.byIcon(Icons.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');
|
controller.addRelay('wss://relay.example.com');
|
||||||
await tester.pumpWidget(createTestWidget());
|
await tester.pumpWidget(createTestWidget());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@@ -178,12 +182,13 @@ void main() {
|
|||||||
|
|
||||||
// Check for loading indicator (may be brief)
|
// Check for loading indicator (may be brief)
|
||||||
expect(find.byType(CircularProgressIndicator), findsWidgets);
|
expect(find.byType(CircularProgressIndicator), findsWidgets);
|
||||||
|
|
||||||
// Wait for health check to complete
|
// Wait for health check to complete
|
||||||
await tester.pumpAndSettle();
|
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());
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
|
||||||
// Trigger an error by adding invalid URL
|
// Trigger an error by adding invalid URL
|
||||||
@@ -198,7 +203,8 @@ void main() {
|
|||||||
expect(find.textContaining('Invalid relay URL'), findsOneWidget);
|
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());
|
await tester.pumpWidget(createTestWidget());
|
||||||
|
|
||||||
// Trigger an error
|
// Trigger an error
|
||||||
@@ -234,4 +240,3 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user