Phase 5 - Relay Management UI complete
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
// Initialize Flutter bindings and sqflite for testing
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
|
||||
late NostrService nostrService;
|
||||
late SyncEngine syncEngine;
|
||||
late LocalStorageService localStorage;
|
||||
late Directory testDir;
|
||||
late String testDbPath;
|
||||
late Directory testCacheDir;
|
||||
late RelayManagementController controller;
|
||||
|
||||
setUp(() async {
|
||||
// Create temporary directory for testing
|
||||
testDir = await Directory.systemTemp.createTemp('relay_test_');
|
||||
testDbPath = path.join(testDir.path, 'test_local_storage.db');
|
||||
testCacheDir = Directory(path.join(testDir.path, 'image_cache'));
|
||||
|
||||
// Initialize local storage
|
||||
localStorage = LocalStorageService(
|
||||
testDbPath: testDbPath,
|
||||
testCacheDir: testCacheDir,
|
||||
);
|
||||
await localStorage.initialize();
|
||||
|
||||
// Create services
|
||||
nostrService = NostrService();
|
||||
syncEngine = SyncEngine(
|
||||
localStorage: localStorage,
|
||||
nostrService: nostrService,
|
||||
);
|
||||
|
||||
// Create controller
|
||||
controller = RelayManagementController(
|
||||
nostrService: nostrService,
|
||||
syncEngine: syncEngine,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
controller.dispose();
|
||||
syncEngine.dispose();
|
||||
nostrService.dispose();
|
||||
await localStorage.close();
|
||||
try {
|
||||
if (await testDir.exists()) {
|
||||
await testDir.delete(recursive: true);
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
group('RelayManagementController', () {
|
||||
test('initial state - empty relay list', () {
|
||||
expect(controller.relays, isEmpty);
|
||||
expect(controller.isSyncing, isFalse);
|
||||
expect(controller.error, isNull);
|
||||
expect(controller.isCheckingHealth, isFalse);
|
||||
});
|
||||
|
||||
test('addRelay - success', () {
|
||||
final url = 'wss://relay.example.com';
|
||||
final result = controller.addRelay(url);
|
||||
|
||||
expect(result, isTrue);
|
||||
expect(controller.relays.length, equals(1));
|
||||
expect(controller.relays[0].url, equals(url));
|
||||
expect(controller.error, isNull);
|
||||
});
|
||||
|
||||
test('addRelay - invalid URL format', () {
|
||||
final result = controller.addRelay('invalid-url');
|
||||
|
||||
expect(result, isFalse);
|
||||
expect(controller.relays, isEmpty);
|
||||
expect(controller.error, isNotNull);
|
||||
expect(controller.error, contains('Invalid relay URL'));
|
||||
});
|
||||
|
||||
test('addRelay - duplicate relay', () {
|
||||
final url = 'wss://relay.example.com';
|
||||
controller.addRelay(url);
|
||||
final result = controller.addRelay(url);
|
||||
|
||||
expect(result, isTrue); // Still returns true, but doesn't add duplicate
|
||||
expect(controller.relays.length, equals(1));
|
||||
});
|
||||
|
||||
test('removeRelay - success', () {
|
||||
final url = 'wss://relay.example.com';
|
||||
controller.addRelay(url);
|
||||
expect(controller.relays.length, equals(1));
|
||||
|
||||
controller.removeRelay(url);
|
||||
expect(controller.relays, isEmpty);
|
||||
expect(controller.error, isNull);
|
||||
});
|
||||
|
||||
test('removeRelay - non-existent relay', () {
|
||||
controller.removeRelay('wss://nonexistent.com');
|
||||
expect(controller.relays, isEmpty);
|
||||
expect(controller.error, isNull);
|
||||
});
|
||||
|
||||
test('clearError - clears error message', () {
|
||||
controller.addRelay('invalid-url');
|
||||
expect(controller.error, isNotNull);
|
||||
|
||||
controller.clearError();
|
||||
expect(controller.error, isNull);
|
||||
});
|
||||
|
||||
test('checkRelayHealth - attempts to connect to relays', () async {
|
||||
// Add a relay (but don't connect to real relay in tests)
|
||||
controller.addRelay('wss://relay.example.com');
|
||||
expect(controller.relays.length, equals(1));
|
||||
expect(controller.relays[0].isConnected, isFalse);
|
||||
|
||||
// Health check will attempt to connect (will fail in test environment, but that's OK)
|
||||
// The method should complete without throwing - it handles connection failures gracefully
|
||||
// Use runZoned to catch any unhandled exceptions that might escape
|
||||
await runZonedGuarded(
|
||||
() async {
|
||||
await controller.checkRelayHealth();
|
||||
},
|
||||
(error, stack) {
|
||||
// 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)
|
||||
expect(controller.relays.length, equals(1));
|
||||
});
|
||||
|
||||
test('triggerManualSync - without sync engine', () async {
|
||||
final controllerWithoutSync = RelayManagementController(
|
||||
nostrService: nostrService,
|
||||
syncEngine: null,
|
||||
);
|
||||
|
||||
final result = await controllerWithoutSync.triggerManualSync();
|
||||
expect(result, isFalse);
|
||||
expect(controllerWithoutSync.error, isNotNull);
|
||||
expect(controllerWithoutSync.error, contains('Sync engine not configured'));
|
||||
|
||||
controllerWithoutSync.dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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;
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
// Initialize Flutter bindings and sqflite for testing
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
|
||||
late NostrService nostrService;
|
||||
late SyncEngine syncEngine;
|
||||
late LocalStorageService localStorage;
|
||||
late Directory testDir;
|
||||
late String testDbPath;
|
||||
late Directory testCacheDir;
|
||||
late RelayManagementController controller;
|
||||
|
||||
setUp(() async {
|
||||
// Create temporary directory for testing
|
||||
testDir = await Directory.systemTemp.createTemp('relay_ui_test_');
|
||||
testDbPath = path.join(testDir.path, 'test_local_storage.db');
|
||||
testCacheDir = Directory(path.join(testDir.path, 'image_cache'));
|
||||
|
||||
// Initialize local storage
|
||||
localStorage = LocalStorageService(
|
||||
testDbPath: testDbPath,
|
||||
testCacheDir: testCacheDir,
|
||||
);
|
||||
await localStorage.initialize();
|
||||
|
||||
// Create services
|
||||
nostrService = NostrService();
|
||||
syncEngine = SyncEngine(
|
||||
localStorage: localStorage,
|
||||
nostrService: nostrService,
|
||||
);
|
||||
|
||||
// Create controller
|
||||
controller = RelayManagementController(
|
||||
nostrService: nostrService,
|
||||
syncEngine: syncEngine,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
controller.dispose();
|
||||
syncEngine.dispose();
|
||||
nostrService.dispose();
|
||||
await localStorage.close();
|
||||
try {
|
||||
if (await testDir.exists()) {
|
||||
await testDir.delete(recursive: true);
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
Widget createTestWidget() {
|
||||
return MaterialApp(
|
||||
home: RelayManagementScreen(controller: controller),
|
||||
);
|
||||
}
|
||||
|
||||
group('RelayManagementScreen', () {
|
||||
testWidgets('displays empty state when no relays', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
expect(find.text('No relays configured'), findsOneWidget);
|
||||
expect(find.text('Add a relay to get started'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.cloud_off), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays relay list correctly', (WidgetTester tester) async {
|
||||
controller.addRelay('wss://relay1.example.com');
|
||||
controller.addRelay('wss://relay2.example.com');
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pump();
|
||||
|
||||
// Relay URLs may appear in both placeholder and list, so use textContaining
|
||||
expect(find.textContaining('wss://relay1.example.com'), findsWidgets);
|
||||
expect(find.textContaining('wss://relay2.example.com'), findsWidgets);
|
||||
// Verify we have relay list items (Cards)
|
||||
expect(find.byType(Card), findsNWidgets(2));
|
||||
expect(find.text('Disconnected'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('adds relay when Add button is pressed', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
// Find and enter relay URL
|
||||
final urlField = find.byType(TextField);
|
||||
expect(urlField, findsOneWidget);
|
||||
await tester.enterText(urlField, 'wss://new-relay.example.com');
|
||||
|
||||
// Find and tap Add button (by text inside)
|
||||
final addButton = find.text('Add');
|
||||
expect(addButton, findsOneWidget);
|
||||
await tester.tap(addButton);
|
||||
await tester.pump();
|
||||
|
||||
// Verify relay was added
|
||||
expect(find.textContaining('wss://new-relay.example.com'), findsWidgets);
|
||||
expect(find.text('Relay added successfully'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows error for invalid URL', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
// Enter invalid URL
|
||||
final urlField = find.byType(TextField);
|
||||
await tester.enterText(urlField, 'invalid-url');
|
||||
|
||||
// Tap Add button
|
||||
final addButton = find.text('Add');
|
||||
await tester.tap(addButton);
|
||||
await tester.pump();
|
||||
|
||||
// Verify error message is shown
|
||||
expect(find.textContaining('Invalid relay URL'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.error), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('removes relay when delete button is pressed', (WidgetTester tester) async {
|
||||
controller.addRelay('wss://relay.example.com');
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pump();
|
||||
|
||||
// Verify relay is in list
|
||||
expect(find.text('wss://relay.example.com'), findsWidgets);
|
||||
expect(controller.relays.length, equals(1));
|
||||
|
||||
// Find and tap delete button
|
||||
final deleteButton = find.byIcon(Icons.delete);
|
||||
expect(deleteButton, findsOneWidget);
|
||||
await tester.tap(deleteButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Verify relay was removed (check controller state)
|
||||
expect(controller.relays, isEmpty);
|
||||
// Verify empty state is shown
|
||||
expect(find.text('No relays configured'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays check health button', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
expect(find.text('Check Health'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.health_and_safety), findsOneWidget);
|
||||
});
|
||||
|
||||
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 {
|
||||
controller.addRelay('wss://relay.example.com');
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pump();
|
||||
|
||||
// Tap check health button
|
||||
final healthButton = find.text('Check Health');
|
||||
await tester.tap(healthButton);
|
||||
await tester.pump();
|
||||
|
||||
// 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 {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
// Trigger an error by adding invalid URL
|
||||
final urlField = find.byType(TextField);
|
||||
await tester.enterText(urlField, 'invalid-url');
|
||||
final addButton = find.text('Add');
|
||||
await tester.tap(addButton);
|
||||
await tester.pump();
|
||||
|
||||
// Verify error container is displayed
|
||||
expect(find.byIcon(Icons.error), findsOneWidget);
|
||||
expect(find.textContaining('Invalid relay URL'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('dismisses error when close button is pressed', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
|
||||
// Trigger an error
|
||||
final urlField = find.byType(TextField);
|
||||
await tester.enterText(urlField, 'invalid-url');
|
||||
final addButton = find.text('Add');
|
||||
await tester.tap(addButton);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Invalid relay URL'), findsOneWidget);
|
||||
|
||||
// Tap close button
|
||||
final closeButtons = find.byIcon(Icons.close);
|
||||
expect(closeButtons, findsOneWidget);
|
||||
await tester.tap(closeButtons);
|
||||
await tester.pump();
|
||||
|
||||
// Error should be cleared
|
||||
expect(find.textContaining('Invalid relay URL'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('displays relay URL in list item', (WidgetTester tester) async {
|
||||
controller.addRelay('wss://relay.example.com');
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pump();
|
||||
|
||||
// Verify relay URL is displayed (may appear in ListTile title)
|
||||
expect(find.textContaining('wss://relay.example.com'), findsWidgets);
|
||||
// Verify status indicator is present
|
||||
expect(find.byType(CircleAvatar), findsWidgets);
|
||||
// Verify we have a relay card
|
||||
expect(find.byType(Card), findsWidgets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user