diff --git a/pubspec.yaml b/pubspec.yaml index 42977dc..3235621 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,9 +22,11 @@ dependencies: flutter_colorpicker: ^1.1.0 dev_dependencies: + sqflite_common_ffi: ^2.3.0 flutter_test: sdk: flutter - + mockito: ^5.4.4 + build_runner: ^2.4.9 flutter_lints: ^5.0.0 change_app_package_name: ^1.4.0 diff --git a/test/Views/home_test.dart b/test/Views/home_test.dart new file mode 100644 index 0000000..ebbd867 --- /dev/null +++ b/test/Views/home_test.dart @@ -0,0 +1,1013 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +import '../../lib/Views/home.dart'; +import '../../lib/card/carddetail.dart'; +import '../../lib/card/cardlist.dart'; +import '../../lib/providers/db.dart'; + +// Generate mocks +@GenerateMocks([DatabaseHelper]) +import 'home_test.mocks.dart'; + +void main() { + group('SortOptions Enum', () { + test('should have exactly three sort options', () { + expect(SortOptions.values.length, 3); + }); + + test('should have correct enum values', () { + expect(SortOptions.values, [ + SortOptions.byName, + SortOptions.byDateCreated, + SortOptions.byUsage, + ]); + }); + + test('enum values should have proper toString representation', () { + expect(SortOptions.byName.toString(), 'SortOptions.byName'); + expect(SortOptions.byDateCreated.toString(), 'SortOptions.byDateCreated'); + expect(SortOptions.byUsage.toString(), 'SortOptions.byUsage'); + }); + }); + + group('Home Widget UI Tests', () { + testWidgets('should display app title when not in search mode', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + expect(find.text('Stashcard'), findsOneWidget); + expect(find.byType(TextField), findsNothing); + }); + + testWidgets('should have correct AppBar structure', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + expect(find.byType(AppBar), findsOneWidget); + expect(find.byType(IconButton), findsNWidgets(3)); // search, donate, popup menu + expect(find.byType(PopupMenuButton), findsOneWidget); + }); + + testWidgets('should show search icon initially', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.byIcon(Icons.close), findsNothing); + }); + + testWidgets('should show donate button with correct icon', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + expect(find.byIcon(Icons.favorite_border), findsOneWidget); + }); + + testWidgets('should show floating action button with add icon', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + expect(find.byType(FloatingActionButton), findsOneWidget); + expect(find.byIcon(Icons.add), findsOneWidget); + }); + + testWidgets('should contain CardGrid widget with default sort option', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + final cardGrid = find.byType(CardGrid); + expect(cardGrid, findsOneWidget); + + final cardGridWidget = tester.widget(cardGrid); + expect(cardGridWidget.selectedOption, SortOptions.byName); + expect(cardGridWidget.searchQuery, ''); + }); + }); + + group('Search Functionality Tests', () { + testWidgets('should enter search mode when search button is pressed', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + expect(find.byType(TextField), findsOneWidget); + expect(find.byIcon(Icons.close), findsOneWidget); + expect(find.text('Stashcard'), findsNothing); + }); + + testWidgets('should configure search TextField correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.autofocus, isTrue); + expect(textField.decoration?.hintText, 'Search...'); + expect(textField.decoration?.border, InputBorder.none); + }); + + testWidgets('should exit search mode when close button is pressed', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Enter search mode + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Enter some text + await tester.enterText(find.byType(TextField), 'test query'); + await tester.pump(); + + // Exit search mode + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + + expect(find.byType(TextField), findsNothing); + expect(find.text('Stashcard'), findsOneWidget); + expect(find.byIcon(Icons.search), findsOneWidget); + }); + + testWidgets('should clear search query when exiting search mode', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Enter search mode and type + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'test query'); + await tester.pump(); + + // Exit and re-enter search mode + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Verify text field is empty + final textField = tester.widget(find.byType(TextField)); + expect(textField.controller?.text, ''); + }); + + testWidgets('should update search query in real-time', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + const testQuery = 'real time search'; + await tester.enterText(find.byType(TextField), testQuery); + await tester.pump(); + + // Verify CardGrid receives updated search query + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, testQuery); + }); + + testWidgets('should handle special characters in search query', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + const specialQuery = '!@#$%^&*()_+-=[]{}|;:,.<>?'; + await tester.enterText(find.byType(TextField), specialQuery); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, specialQuery); + }); + + testWidgets('should handle empty search query', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Enter text then clear it + await tester.enterText(find.byType(TextField), 'test'); + await tester.pump(); + await tester.enterText(find.byType(TextField), ''); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, ''); + }); + }); + + group('Donate Dialog Tests', () { + testWidgets('should show donate dialog when donate button is pressed', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pump(); + + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Donate'), findsWidgets); + expect(find.byIcon(Icons.favorite), findsOneWidget); + }); + + testWidgets('should configure donate dialog correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pump(); + + expect(find.text('Donate'), findsNWidgets(2)); // Title and button + expect(find.textContaining("I'm a student"), findsOneWidget); + expect(find.text('Enjoy the app!'), findsOneWidget); + expect(find.text('Close'), findsOneWidget); + }); + + testWidgets('should have correct dialog styling', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pump(); + + final dialog = tester.widget(find.byType(AlertDialog)); + expect(dialog.title, isA()); + expect(dialog.icon, isA()); + expect(dialog.content, isA()); + expect(dialog.actions?.length, 2); + }); + + testWidgets('should close donate dialog when close button is pressed', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pump(); + + await tester.tap(find.text('Close')); + await tester.pump(); + + expect(find.byType(AlertDialog), findsNothing); + }); + + testWidgets('should have FilledButton for donate action', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.favorite_border)); + await tester.pump(); + + expect(find.byType(FilledButton), findsOneWidget); + expect(find.byType(OutlinedButton), findsOneWidget); + }); + }); + + group('Sort Menu Tests', () { + testWidgets('should show popup menu with all sort options', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pump(); + + expect(find.text('Sort by name'), findsOneWidget); + expect(find.text('Sort by date created'), findsOneWidget); + expect(find.text('Sort by usage'), findsOneWidget); + }); + + testWidgets('should have correct initial sort option', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + final popupButton = tester.widget>( + find.byType(PopupMenuButton) + ); + expect(popupButton.initialValue, SortOptions.byName); + }); + + testWidgets('should update sort option when menu item is selected', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Open popup menu + await tester.tap(find.byType(PopupMenuButton)); + await tester.pump(); + + // Select 'Sort by usage' + await tester.tap(find.text('Sort by usage')); + await tester.pump(); + + // Verify CardGrid receives the updated sort option + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.selectedOption, SortOptions.byUsage); + }); + + testWidgets('should close menu after selection', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pump(); + + await tester.tap(find.text('Sort by date created')); + await tester.pump(); + + // Menu should be closed + expect(find.text('Sort by name'), findsNothing); + expect(find.text('Sort by date created'), findsNothing); + expect(find.text('Sort by usage'), findsNothing); + }); + }); + + group('Navigation Tests', () { + testWidgets('should navigate to CardList when FAB is pressed', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: const Home(), + routes: { + '/cardlist': (context) => const CardList(), + }, + ), + ); + + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + + expect(find.byType(CardList), findsOneWidget); + }); + + testWidgets('should use MaterialPageRoute for navigation', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // We can't directly test the route type, but we can verify the navigation works + await tester.tap(find.byType(FloatingActionButton)); + await tester.pump(); + + // The navigation should trigger without errors + expect(tester.takeException(), isNull); + }); + }); + + group('CardGrid Widget Tests', () { + late MockDatabaseHelper mockDb; + late List mockCards; + + setUp(() { + mockDb = MockDatabaseHelper(); + mockCards = [ + UserCard( + id: 1, + name: 'Test Card 1', + code: '123456789', + usage: 5, + createdAt: DateTime.now(), + symbology: 'CODE128', + ), + UserCard( + id: 2, + name: 'Test Card 2', + code: '987654321', + usage: 3, + createdAt: DateTime.now().subtract(const Duration(days: 1)), + symbology: 'QR', + ), + UserCard( + id: 3, + name: 'Search Test Card', + code: '555555555', + usage: 1, + createdAt: DateTime.now().subtract(const Duration(days: 2)), + symbology: 'EAN13', + ), + ]; + }); + + testWidgets('should create CardGrid with required parameters', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CardGrid(selectedOption: SortOptions.byDateCreated), + ), + ); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.selectedOption, SortOptions.byDateCreated); + expect(cardGrid.searchQuery, ''); + }); + + testWidgets('should accept custom search query', (WidgetTester tester) async { + const testQuery = 'custom search'; + await tester.pumpWidget( + const MaterialApp( + home: CardGrid( + selectedOption: SortOptions.byUsage, + searchQuery: testQuery, + ), + ), + ); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, testQuery); + }); + + testWidgets('should show future builder initially', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CardGrid(selectedOption: SortOptions.byName), + ), + ); + + expect(find.byType(FutureBuilder), findsOneWidget); + }); + + testWidgets('should display error message when database fails', (WidgetTester tester) async { + // Create a widget that uses our mock + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureBuilder>( + future: Future.error('Database error'), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text("Chyba: ${snapshot.error}")); + } + return const CircularProgressIndicator(); + }, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(); + + expect(find.textContaining('Chyba:'), findsOneWidget); + }); + + testWidgets('should display no cards message when list is empty', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureBuilder>( + future: Future.value([]), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text("Chyba: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text("Žádné karty")); + } + return const Text('Has data'); + }, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Žádné karty'), findsOneWidget); + }); + + testWidgets('should display cards when data is loaded', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureBuilder>( + future: Future.value(mockCards), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text("Chyba: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text("Žádné karty")); + } + + final userCards = snapshot.data!; + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: userCards.length, + itemBuilder: (context, index) { + final userCard = userCards[index]; + return Card( + child: Center( + child: Text(userCard.name), + ), + ); + }, + ); + }, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(); + + expect(find.byType(GridView), findsOneWidget); + expect(find.text('Test Card 1'), findsOneWidget); + expect(find.text('Test Card 2'), findsOneWidget); + expect(find.text('Search Test Card'), findsOneWidget); + }); + + testWidgets('should filter cards based on search query', (WidgetTester tester) async { + const searchQuery = 'search'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureBuilder>( + future: Future.value(mockCards), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text("Chyba: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text("Žádné karty")); + } + + final userCards = snapshot.data!; + final filteredCards = userCards.where((userCard) { + return searchQuery.isEmpty || userCard.name.toLowerCase().contains(searchQuery.toLowerCase()); + }).toList(); + + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: filteredCards.length, + itemBuilder: (context, index) { + final userCard = filteredCards[index]; + return Card( + child: Center( + child: Text(userCard.name), + ), + ); + }, + ); + }, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(); + + // Only 'Search Test Card' should be visible + expect(find.text('Search Test Card'), findsOneWidget); + expect(find.text('Test Card 1'), findsNothing); + expect(find.text('Test Card 2'), findsNothing); + }); + + testWidgets('should handle case-insensitive search', (WidgetTester tester) async { + const searchQuery = 'SEARCH'; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureBuilder>( + future: Future.value(mockCards), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text("Chyba: ${snapshot.error}")); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text("Žádné karty")); + } + + final userCards = snapshot.data!; + final filteredCards = userCards.where((userCard) { + return searchQuery.isEmpty || userCard.name.toLowerCase().contains(searchQuery.toLowerCase()); + }).toList(); + + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: filteredCards.length, + itemBuilder: (context, index) { + final userCard = filteredCards[index]; + return Card( + child: Center( + child: Text(userCard.name), + ), + ); + }, + ); + }, + ), + ), + ), + ); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Search Test Card'), findsOneWidget); + }); + + testWidgets('should have correct grid configuration', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: GridView.builder( + padding: const EdgeInsets.all(20), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1.5, + ), + itemCount: 3, + itemBuilder: (context, index) { + return const Card(elevation: 2); + }, + ), + ), + ), + ); + + final gridView = tester.widget(find.byType(GridView)); + final delegate = gridView.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount; + + expect(delegate.crossAxisCount, 2); + expect(delegate.crossAxisSpacing, 10); + expect(delegate.mainAxisSpacing, 10); + expect(delegate.childAspectRatio, 1.5); + }); + + testWidgets('should have RefreshIndicator', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: RefreshIndicator( + onRefresh: () async {}, + child: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemCount: 0, + itemBuilder: (context, index) => const SizedBox(), + ), + ), + ), + ), + ); + + expect(find.byType(RefreshIndicator), findsOneWidget); + }); + }); + + group('State Management Tests', () { + testWidgets('should maintain search state correctly', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Initial state + expect(find.byIcon(Icons.search), findsOneWidget); + + // Toggle search on + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + expect(find.byIcon(Icons.close), findsOneWidget); + + // Toggle search off + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + expect(find.byIcon(Icons.search), findsOneWidget); + }); + + testWidgets('should handle multiple state changes', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Rapid state changes + for (int i = 0; i < 3; i++) { + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + } + + // Should end in initial state + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.text('Stashcard'), findsOneWidget); + }); + + testWidgets('should preserve search text during typing', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Type incrementally + await tester.enterText(find.byType(TextField), 'a'); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'ab'); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'abc'); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, 'abc'); + }); + }); + + group('Integration Tests', () { + testWidgets('should pass updated search query to CardGrid', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + const testQuery = 'integration test query'; + await tester.enterText(find.byType(TextField), testQuery); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, testQuery); + }); + + testWidgets('should pass updated sort option to CardGrid', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pump(); + await tester.tap(find.text('Sort by date created')); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.selectedOption, SortOptions.byDateCreated); + }); + + testWidgets('should maintain both search and sort state simultaneously', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Set search query + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'test query'); + await tester.pump(); + + // Change sort option + await tester.tap(find.byIcon(Icons.close)); // Exit search to access menu + await tester.pump(); + await tester.tap(find.byType(PopupMenuButton)); + await tester.pump(); + await tester.tap(find.text('Sort by usage')); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.selectedOption, SortOptions.byUsage); + // Search query should be cleared when exiting search mode + expect(cardGrid.searchQuery, ''); + }); + }); + + group('Edge Cases and Error Handling', () { + testWidgets('should handle rapid button taps gracefully', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Rapid search button taps + for (int i = 0; i < 10; i++) { + await tester.tap(find.byIcon(i % 2 == 0 ? Icons.search : Icons.close)); + await tester.pump(); + } + + // Should end in search mode (even number of taps) + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets('should handle Unicode characters in search', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + const unicodeQuery = '测试 🎯 ñoño'; + await tester.enterText(find.byType(TextField), unicodeQuery); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, unicodeQuery); + }); + + testWidgets('should handle very long search queries', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + final longQuery = 'a' * 1000; + await tester.enterText(find.byType(TextField), longQuery); + await tester.pump(); + + final cardGrid = tester.widget(find.byType(CardGrid)); + expect(cardGrid.searchQuery, longQuery); + }); + + testWidgets('should handle widget rebuild without errors', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Force multiple rebuilds + for (int i = 0; i < 5; i++) { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + } + + expect(find.byType(Home), findsOneWidget); + expect(tester.takeException(), isNull); + }); + }); + + group('Accessibility Tests', () { + testWidgets('should have proper semantics for screen readers', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // Verify important interactive elements are present + expect(find.byType(IconButton), findsNWidgets(3)); + expect(find.byType(FloatingActionButton), findsOneWidget); + expect(find.byType(PopupMenuButton), findsOneWidget); + }); + + testWidgets('should support keyboard navigation for search field', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.autofocus, isTrue); + }); + + testWidgets('should provide proper button semantics', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp(home: Home()), + ); + + // All buttons should be tappable + expect(find.byType(IconButton), findsNWidgets(3)); + expect(find.byType(FloatingActionButton), findsOneWidget); + + // Test that buttons can be found and tapped + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + + expect(tester.takeException(), isNull); + }); + }); + + group('UserCard Model Tests', () { + test('should create UserCard with all required fields', () { + final createdAt = DateTime.now(); + final card = UserCard( + id: 1, + name: 'Test Card', + code: '123456789', + usage: 5, + createdAt: createdAt, + symbology: 'CODE128', + ); + + expect(card.id, 1); + expect(card.name, 'Test Card'); + expect(card.code, '123456789'); + expect(card.usage, 5); + expect(card.createdAt, createdAt); + expect(card.symbology, 'CODE128'); + }); + + test('should create UserCard without id', () { + final createdAt = DateTime.now(); + const card = UserCard( + name: 'Test Card', + code: '123456789', + usage: 0, + createdAt: createdAt, + symbology: 'QR', + ); + + expect(card.id, isNull); + expect(card.name, 'Test Card'); + }); + + test('should create copy of UserCard with updated fields', () { + final originalDate = DateTime.now(); + final card = UserCard( + id: 1, + name: 'Original', + code: '123', + usage: 1, + createdAt: originalDate, + symbology: 'CODE128', + ); + + final updatedCard = card.copyWith( + name: 'Updated', + usage: 5, + ); + + expect(updatedCard.id, 1); + expect(updatedCard.name, 'Updated'); + expect(updatedCard.code, '123'); + expect(updatedCard.usage, 5); + expect(updatedCard.createdAt, originalDate); + expect(updatedCard.symbology, 'CODE128'); + }); + + test('should convert UserCard to map correctly', () { + final createdAt = DateTime.now(); + final card = UserCard( + id: 1, + name: 'Test Card', + code: '123456789', + usage: 5, + createdAt: createdAt, + symbology: 'CODE128', + ); + + final map = card.toMap(); + + expect(map['id'], 1); + expect(map['name'], 'Test Card'); + expect(map['code'], '123456789'); + expect(map['usage'], 5); + expect(map['created_at'], createdAt.toIso8601String()); + expect(map['symbology'], 'CODE128'); + }); + + test('should have proper string representation', () { + final createdAt = DateTime.now(); + final card = UserCard( + id: 1, + name: 'Test Card', + code: '123456789', + usage: 5, + createdAt: createdAt, + symbology: 'CODE128', + ); + + final cardString = card.toString(); + expect(cardString, contains('Test Card')); + expect(cardString, contains('123456789')); + expect(cardString, contains('5')); + expect(cardString, contains('CODE128')); + }); + }); +} \ No newline at end of file diff --git a/test/Views/home_test.mocks.dart b/test/Views/home_test.mocks.dart new file mode 100644 index 0000000..b4bf2cd --- /dev/null +++ b/test/Views/home_test.mocks.dart @@ -0,0 +1,5 @@ +// Mock file placeholder - run 'flutter packages pub run build_runner build' to generate mocks +import 'package:mockito/mockito.dart'; +import '../../lib/providers/db.dart'; + +class MockDatabaseHelper extends Mock implements DatabaseHelper {} \ No newline at end of file diff --git a/test/card/carddetail_test.dart b/test/card/carddetail_test.dart new file mode 100644 index 0000000..338118a --- /dev/null +++ b/test/card/carddetail_test.dart @@ -0,0 +1,705 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:stashcard/card/carddetail.dart'; +import 'package:stashcard/card/cardedit.dart'; +import 'package:stashcard/providers/db.dart'; +import 'package:stashcard/Views/home.dart'; +import 'package:syncfusion_flutter_barcodes/barcodes.dart'; + +// Generate mocks +@GenerateMocks([DatabaseHelper]) +import 'carddetail_test.mocks.dart'; + +void main() { + group('CardDetail Widget Tests', () { + late MockDatabaseHelper mockDatabaseHelper; + late UserCard testCard; + + setUp(() { + mockDatabaseHelper = MockDatabaseHelper(); + testCard = UserCard( + id: 1, + name: 'Test Card', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + }); + + testWidgets('should display loading indicator when card is null', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(any)).thenAnswer((_) async => null); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + + // Assert + expect(find.byType(CircularProgressIndicator), findsOneWidget); + expect(find.text('Test Card'), findsNothing); + }); + + testWidgets('should display card details when card is loaded', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Test Card'), findsOneWidget); + expect(find.text('card type: code128'), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('should display app bar with title "card Detail"', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.widgetWithText(AppBar, 'card Detail'), findsOneWidget); + }); + + testWidgets('should display popup menu button in app bar', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(PopupMenuButton), findsOneWidget); + }); + + testWidgets('should show popup menu options when menu button is tapped', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Edit'), findsOneWidget); + expect(find.text('Share'), findsOneWidget); + expect(find.text('Delete'), findsOneWidget); + }); + + testWidgets('should navigate to CardEdit when Edit menu item is tapped', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(CardEdit), findsOneWidget); + }); + + testWidgets('should show delete confirmation dialog when Delete menu item is tapped', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.text('Delete card'), findsOneWidget); + expect(find.text('Are you sure you want to delete this card?'), findsOneWidget); + expect(find.text('Delete'), findsAtLeastNWidgets(1)); + expect(find.text('Cancel'), findsOneWidget); + }); + + testWidgets('should dismiss dialog when Cancel button is tapped in delete confirmation', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Cancel')); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(AlertDialog), findsNothing); + }); + + testWidgets('should call deleteUserCard and navigate to Home when Delete is confirmed', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + when(mockDatabaseHelper.deleteUserCard(1)).thenAnswer((_) async => {}); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(OutlinedButton, 'Delete')); + await tester.pumpAndSettle(); + + // Assert + verify(mockDatabaseHelper.deleteUserCard(1)).called(1); + expect(find.byType(Home), findsOneWidget); + }); + + testWidgets('should handle null cardId gracefully', (WidgetTester tester) async { + // Act & Assert - should throw error when accessing null cardId + expect(() async { + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: null), + ), + ); + await tester.pumpAndSettle(); + }, throwsA(isA())); + }); + + testWidgets('should refresh card data on PopScope callback', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Simulate navigation back + final navigator = Navigator.of(tester.element(find.byType(CardDetail))); + navigator.pop(); + await tester.pumpAndSettle(); + + // Assert - verify getOneCard was called during initial load + verify(mockDatabaseHelper.getOneCard(1)).called(greaterThanOrEqualTo(1)); + }); + + testWidgets('should display barcode with correct properties', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final barcodeGenerator = tester.widget(find.byType(SfBarcodeGenerator)); + expect(barcodeGenerator.value, equals('1234567890')); + expect(barcodeGenerator.showValue, isTrue); + expect(barcodeGenerator.barColor, equals(Colors.black)); + expect(barcodeGenerator.textSpacing, equals(10)); + }); + + testWidgets('should handle different card symbologies correctly', (WidgetTester tester) async { + // Test cases for different symbologies + final testCases = ['code39', 'code93', 'code128', 'ean8', 'ean13', 'upcA', 'upcE', 'qrCode']; + + for (String symbology in testCases) { + // Arrange + final cardWithSymbology = UserCard( + id: 1, + name: 'Test Card', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: symbology, + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => cardWithSymbology); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('card type: $symbology'), findsOneWidget); + expect(symbologies.containsKey(symbology), isTrue); + + // Reset for next iteration + reset(mockDatabaseHelper); + } + }); + + testWidgets('should handle async card loading correctly', (WidgetTester tester) async { + // Arrange + final completer = Completer(); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) => completer.future); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pump(); + + // Assert - should show loading initially + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // Complete the future + completer.complete(testCard); + await tester.pumpAndSettle(); + + // Assert - should show card details + expect(find.text('Test Card'), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + + testWidgets('should handle database errors gracefully', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenThrow(Exception('Database error')); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pump(); + + // Assert - widget should still render, showing loading state due to error + expect(find.byType(CardDetail), findsOneWidget); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + + testWidgets('should update selected option when menu item is selected', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + // Find and tap the Share menu item + await tester.tap(find.text('Share')); + await tester.pumpAndSettle(); + + // Assert - verify the popup menu is still present + expect(find.byType(PopupMenuButton), findsOneWidget); + }); + + testWidgets('should display proper layout structure', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert layout structure + expect(find.byType(PopScope), findsOneWidget); + expect(find.byType(Scaffold), findsOneWidget); + expect(find.byType(Column), findsOneWidget); + expect(find.byType(Container), findsOneWidget); + expect(find.byType(SizedBox), findsAtLeastNWidgets(1)); + }); + + testWidgets('should have correct container styling for barcode', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + final container = tester.widget(find.byType(Container)); + expect(container.color, equals(Colors.white)); + expect(container.padding, equals(const EdgeInsets.all(10))); + }); + }); + + group('CardOptions Enum Tests', () { + test('should contain all expected values', () { + expect(CardOptions.values.length, equals(3)); + expect(CardOptions.values, contains(CardOptions.edit)); + expect(CardOptions.values, contains(CardOptions.share)); + expect(CardOptions.values, contains(CardOptions.delete)); + }); + + test('should have correct string representations', () { + expect(CardOptions.edit.toString(), contains('edit')); + expect(CardOptions.share.toString(), contains('share')); + expect(CardOptions.delete.toString(), contains('delete')); + }); + + test('should be comparable and hashable', () { + expect(CardOptions.edit == CardOptions.edit, isTrue); + expect(CardOptions.edit == CardOptions.share, isFalse); + expect(CardOptions.edit.hashCode, equals(CardOptions.edit.hashCode)); + }); + }); + + group('Symbologies Map Tests', () { + test('should contain all expected symbology types', () { + expect(symbologies.keys, contains('code39')); + expect(symbologies.keys, contains('code93')); + expect(symbologies.keys, contains('code128')); + expect(symbologies.keys, contains('ean8')); + expect(symbologies.keys, contains('ean13')); + expect(symbologies.keys, contains('upcA')); + expect(symbologies.keys, contains('upcE')); + expect(symbologies.keys, contains('qrCode')); + }); + + test('should have correct number of symbologies', () { + expect(symbologies.length, equals(8)); + }); + + test('should map to correct Symbology instances', () { + expect(symbologies['code39'], isA()); + expect(symbologies['code93'], isA()); + expect(symbologies['code128'], isA()); + expect(symbologies['ean8'], isA()); + expect(symbologies['ean13'], isA()); + expect(symbologies['upcA'], isA()); + expect(symbologies['upcE'], isA()); + expect(symbologies['qrCode'], isA()); + }); + + test('should handle case sensitivity correctly', () { + expect(symbologies['CODE39'], isNull); + expect(symbologies['qrcode'], isNull); + expect(symbologies['qrCode'], isNotNull); + }); + + test('should return null for invalid symbology keys', () { + expect(symbologies['invalid'], isNull); + expect(symbologies[''], isNull); + expect(symbologies['code128a'], isNull); + }); + + test('should have immutable symbology instances', () { + final code39_1 = symbologies['code39']; + final code39_2 = symbologies['code39']; + expect(identical(code39_1, code39_2), isTrue); + }); + }); + + group('Edge Cases and Error Handling', () { + late MockDatabaseHelper mockDatabaseHelper; + + setUp(() { + mockDatabaseHelper = MockDatabaseHelper(); + }); + + testWidgets('should handle card with empty name', (WidgetTester tester) async { + // Arrange + final emptyNameCard = UserCard( + id: 1, + name: '', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => emptyNameCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('card type: code128'), findsOneWidget); + expect(find.byType(SfBarcodeGenerator), findsOneWidget); + }); + + testWidgets('should handle card with empty code', (WidgetTester tester) async { + // Arrange + final emptyCodeCard = UserCard( + id: 1, + name: 'Test Card', + code: '', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => emptyCodeCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Test Card'), findsOneWidget); + expect(find.byType(SfBarcodeGenerator), findsOneWidget); + final barcodeGenerator = tester.widget(find.byType(SfBarcodeGenerator)); + expect(barcodeGenerator.value, equals('')); + }); + + testWidgets('should handle invalid symbology gracefully', (WidgetTester tester) async { + // Arrange + final invalidSymbologyCard = UserCard( + id: 1, + name: 'Test Card', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'invalid_symbology', + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => invalidSymbologyCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Test Card'), findsOneWidget); + expect(find.text('card type: invalid_symbology'), findsOneWidget); + final barcodeGenerator = tester.widget(find.byType(SfBarcodeGenerator)); + expect(barcodeGenerator.symbology, isNull); // Should be null for invalid symbology + }); + + testWidgets('should handle very long card names', (WidgetTester tester) async { + // Arrange + final longNameCard = UserCard( + id: 1, + name: 'This is a very long card name that might cause layout issues in the UI', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => longNameCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('This is a very long card name that might cause layout issues in the UI'), findsOneWidget); + }); + + testWidgets('should handle special characters in card name and code', (WidgetTester tester) async { + // Arrange + final specialCharCard = UserCard( + id: 1, + name: 'Test Card @#\$%^&*()', + code: '12345-67890_TEST', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async => specialCharCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('Test Card @#\$%^&*()'), findsOneWidget); + final barcodeGenerator = tester.widget(find.byType(SfBarcodeGenerator)); + expect(barcodeGenerator.value, equals('12345-67890_TEST')); + }); + + testWidgets('should handle network/database timeout scenarios', (WidgetTester tester) async { + // Arrange + when(mockDatabaseHelper.getOneCard(1)).thenAnswer((_) async { + await Future.delayed(Duration(seconds: 5)); + throw TimeoutException('Database timeout', Duration(seconds: 5)); + }); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pump(); + + // Assert - should show loading indicator during timeout + expect(find.byType(CircularProgressIndicator), findsOneWidget); + }); + }); + + group('Integration Tests', () { + testWidgets('should complete full delete workflow', (WidgetTester tester) async { + // Arrange + final mockDb = MockDatabaseHelper(); + final testCard = UserCard( + id: 1, + name: 'Integration Test Card', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDb.getOneCard(1)).thenAnswer((_) async => testCard); + when(mockDb.deleteUserCard(1)).thenAnswer((_) async => {}); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Open menu + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + // Tap delete + await tester.tap(find.text('Delete')); + await tester.pumpAndSettle(); + + // Confirm delete + await tester.tap(find.widgetWithText(OutlinedButton, 'Delete')); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(Home), findsOneWidget); + verify(mockDb.deleteUserCard(1)).called(1); + }); + + testWidgets('should complete full edit workflow navigation', (WidgetTester tester) async { + // Arrange + final mockDb = MockDatabaseHelper(); + final testCard = UserCard( + id: 1, + name: 'Edit Test Card', + code: '1234567890', + usage: 0, + createdAt: DateTime.now(), + symbology: 'code128', + ); + when(mockDb.getOneCard(1)).thenAnswer((_) async => testCard); + + // Act + await tester.pumpWidget( + MaterialApp( + home: CardDetail(cardId: 1), + ), + ); + await tester.pumpAndSettle(); + + // Open menu and select edit + await tester.tap(find.byType(PopupMenuButton)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Edit')); + await tester.pumpAndSettle(); + + // Assert navigation to CardEdit + expect(find.byType(CardEdit), findsOneWidget); + }); + }); +} \ No newline at end of file diff --git a/test/card/cardedit_test.dart b/test/card/cardedit_test.dart new file mode 100644 index 0000000..642fba7 --- /dev/null +++ b/test/card/cardedit_test.dart @@ -0,0 +1,425 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:stashcard/card/cardedit.dart'; +import 'package:stashcard/providers/db.dart'; + +// Generate mocks +@GenerateMocks([DatabaseHelper]) +import 'cardedit_test.mocks.dart'; + +void main() { + group('CardEdit Widget Tests', () { + late MockDatabaseHelper mockDb; + late UserCard testCard; + + setUp(() { + mockDb = MockDatabaseHelper(); + testCard = UserCard( + id: 1, + name: 'Test Card', + // Add other required properties based on UserCard structure + ); + }); + + testWidgets('should display edit card title in app bar', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + expect(find.text('Edit card'), findsOneWidget); + expect(find.byType(AppBar), findsOneWidget); + }); + + testWidgets('should initialize text field with existing card name', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final textField = find.byType(TextFormField); + expect(textField, findsOneWidget); + + final textFormField = tester.widget(textField); + expect(textFormField.controller?.text, equals('Test Card')); + }); + + testWidgets('should have proper form structure and widgets', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + expect(find.byType(Scaffold), findsOneWidget); + expect(find.byType(Form), findsOneWidget); + expect(find.byType(Column), findsOneWidget); + expect(find.byType(TextFormField), findsOneWidget); + expect(find.byType(FilledButton), findsOneWidget); + expect(find.text('Save'), findsOneWidget); + }); + + testWidgets('should have correct input decoration with label', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.decoration?.labelText, equals('Name')); + }); + + testWidgets('should validate empty input and show error message', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Clear the text field + await tester.enterText(find.byType(TextFormField), ''); + await tester.pump(); + + // Trigger validation by finding the validator function + final textField = tester.widget(find.byType(TextFormField)); + final validationResult = textField.validator?.call(''); + + expect(validationResult, equals('Please enter a name')); + }); + + testWidgets('should validate null input and show error message', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + final validationResult = textField.validator?.call(null); + + expect(validationResult, equals('Please enter a name')); + }); + + testWidgets('should pass validation with valid input', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + final validationResult = textField.validator?.call('Valid Name'); + + expect(validationResult, isNull); + }); + + testWidgets('should allow text input and update controller', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + const newText = 'Updated Card Name'; + await tester.enterText(find.byType(TextFormField), newText); + await tester.pump(); + + expect(find.text(newText), findsOneWidget); + }); + + testWidgets('should have proper padding and layout', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final bodyPadding = tester.widget(find.descendant( + of: find.byType(Scaffold), + matching: find.byType(Padding), + ).first); + expect(bodyPadding.padding, equals(const EdgeInsets.all(8.0))); + + final buttonPadding = tester.widget(find.descendant( + of: find.byType(Column), + matching: find.byType(Padding), + )); + expect(buttonPadding.padding, equals(const EdgeInsets.only(top: 8.0))); + }); + + testWidgets('should have column with correct cross axis alignment', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final column = tester.widget(find.byType(Column)); + expect(column.crossAxisAlignment, equals(CrossAxisAlignment.end)); + }); + + group('Save Button Tests', () { + testWidgets('should have save button with correct text', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final saveButton = find.byType(FilledButton); + expect(saveButton, findsOneWidget); + expect(find.descendant(of: saveButton, matching: find.text('Save')), findsOneWidget); + }); + + testWidgets('should trigger save action when button is pressed', (WidgetTester tester) async { + // This test would require dependency injection or mocking framework + // For now, we'll test that the button exists and can be tapped + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final saveButton = find.byType(FilledButton); + expect(saveButton, findsOneWidget); + + // Verify button is tappable + await tester.tap(saveButton); + await tester.pump(); + + // Note: Full integration testing would require mocking DatabaseHelper + // and verifying the updateUserCard call and navigation + }); + }); + + group('Widget Lifecycle Tests', () { + testWidgets('should properly initialize text controller in initState', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + final state = tester.state<_CardEditState>(find.byType(CardEdit)); + expect(state.newCardName.text, equals(testCard.name)); + }); + + testWidgets('should dispose text controller properly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Get reference to controller + final state = tester.state<_CardEditState>(find.byType(CardEdit)); + final controller = state.newCardName; + + // Navigate away to trigger dispose + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: Text('Different Page')), + ), + ); + + // Verify controller is disposed (this would throw if accessed after dispose) + expect(() => controller.text, throwsFlutterError); + }); + }); + + group('Edge Cases and Error Handling', () { + testWidgets('should handle card with empty name', (WidgetTester tester) async { + final emptyNameCard = UserCard( + id: 2, + name: '', + ); + + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: emptyNameCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, equals('')); + }); + + testWidgets('should handle card with null name gracefully', (WidgetTester tester) async { + // Assuming UserCard can have null name in some cases + final nullNameCard = UserCard( + id: 3, + name: null, + ); + + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: nullNameCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, equals('')); + }); + + testWidgets('should handle very long card names', (WidgetTester tester) async { + final longName = 'A' * 1000; // Very long name + final longNameCard = UserCard( + id: 4, + name: longName, + ); + + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: longNameCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, equals(longName)); + }); + + testWidgets('should handle special characters in card name', (WidgetTester tester) async { + final specialCharName = 'Card @#$%^&*()_+-={}[]|\\:";\'<>?,./'; + final specialCharCard = UserCard( + id: 5, + name: specialCharName, + ); + + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: specialCharCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, equals(specialCharName)); + }); + + testWidgets('should handle unicode characters in card name', (WidgetTester tester) async { + final unicodeName = '测试卡片 🃏 ñáéíóú'; + final unicodeCard = UserCard( + id: 6, + name: unicodeName, + ); + + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: unicodeCard), + ), + ); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, equals(unicodeName)); + }); + }); + + group('Accessibility Tests', () { + testWidgets('should have proper semantics for screen readers', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Test that semantic labels are present + expect(find.text('Name'), findsOneWidget); + expect(find.text('Save'), findsOneWidget); + expect(find.text('Edit card'), findsOneWidget); + }); + + testWidgets('should be navigable with keyboard/screen reader', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Verify focusable elements exist + expect(find.byType(TextFormField), findsOneWidget); + expect(find.byType(FilledButton), findsOneWidget); + }); + }); + + group('Integration-like Tests', () { + testWidgets('should update text field when typing', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Start with original name + expect(find.text('Test Card'), findsOneWidget); + + // Type new text + await tester.enterText(find.byType(TextFormField), 'New Card Name'); + await tester.pump(); + + // Verify text changed + expect(find.text('New Card Name'), findsOneWidget); + expect(find.text('Test Card'), findsNothing); + }); + + testWidgets('should clear text field when cleared', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Clear the field + await tester.enterText(find.byType(TextFormField), ''); + await tester.pump(); + + final textField = tester.widget(find.byType(TextFormField)); + expect(textField.controller?.text, isEmpty); + }); + + testWidgets('should maintain state during rebuilds', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + + // Enter new text + const newText = 'Modified Name'; + await tester.enterText(find.byType(TextFormField), newText); + await tester.pump(); + + // Force a rebuild by changing parent widget + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.dark(), // Change theme to force rebuild + home: CardEdit(card: testCard), + ), + ); + + // Text should persist through rebuild + expect(find.text(newText), findsOneWidget); + }); + }); + + group('Performance Tests', () { + testWidgets('should not have expensive operations in build method', (WidgetTester tester) async { + // Build widget multiple times to ensure no expensive operations + for (int i = 0; i < 10; i++) { + await tester.pumpWidget( + MaterialApp( + home: CardEdit(card: testCard), + ), + ); + await tester.pump(); + } + + // If we get here without timeout, build method is efficient + expect(find.byType(CardEdit), findsOneWidget); + }); + }); + }); +} \ No newline at end of file diff --git a/test/card/cardlist_test.dart b/test/card/cardlist_test.dart new file mode 100644 index 0000000..77aea8e --- /dev/null +++ b/test/card/cardlist_test.dart @@ -0,0 +1,681 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stashcard/card/cardlist.dart'; +import 'package:stashcard/card/scanner.dart'; + +void main() { + group('loadCards function tests', () { + setUp(() { + // Mock the asset bundle for testing + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage( + 'Visa\nMasterCard\nAmerican Express\nDiscover\nJCB\nDiners Club\nUnionPay\n' + ); + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', null); + }); + + test('should load and filter cards correctly with empty filter', () async { + final result = await loadCards(''); + expect(result, isA>()); + expect(result.length, 7); + expect(result, contains('Visa')); + expect(result, contains('MasterCard')); + expect(result, contains('American Express')); + }); + + test('should filter cards case-insensitively', () async { + final result = await loadCards('visa'); + expect(result, hasLength(1)); + expect(result.first, 'Visa'); + }); + + test('should filter cards with partial matches', () async { + final result = await loadCards('card'); + expect(result, hasLength(1)); + expect(result.first, 'MasterCard'); + }); + + test('should return empty list when no matches found', () async { + final result = await loadCards('nonexistent'); + expect(result, isEmpty); + }); + + test('should handle uppercase filter correctly', () async { + final result = await loadCards('AMERICAN'); + expect(result, hasLength(1)); + expect(result.first, 'American Express'); + }); + + test('should handle mixed case filter correctly', () async { + final result = await loadCards('DiNeRs'); + expect(result, hasLength(1)); + expect(result.first, 'Diners Club'); + }); + + test('should filter multiple results', () async { + final result = await loadCards('e'); + expect(result.length, greaterThan(1)); + expect(result, contains('American Express')); + expect(result, contains('Discover')); + }); + + test('should handle empty lines and whitespace in asset file', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage( + 'Visa\n \nMasterCard\n\n American Express \n' + ); + } + return null; + }); + + final result = await loadCards(''); + expect(result, hasLength(3)); + expect(result, contains('Visa')); + expect(result, contains('MasterCard')); + expect(result, contains('American Express')); + }); + + test('should handle asset file with only whitespace', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage(' \n \n '); + } + return null; + }); + + final result = await loadCards(''); + expect(result, isEmpty); + }); + + test('should handle special characters in filter', () async { + final result = await loadCards('&'); + expect(result, isEmpty); + }); + + test('should handle numeric filter', () async { + final result = await loadCards('123'); + expect(result, isEmpty); + }); + + test('should handle whitespace in filter', () async { + final result = await loadCards(' visa '); + expect(result, hasLength(1)); + expect(result.first, 'Visa'); + }); + }); + + group('CardList widget tests', () { + testWidgets('should display app bar with correct title', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + expect(find.text('Add card'), findsOneWidget); + expect(find.byType(AppBar), findsOneWidget); + }); + + testWidgets('should toggle search mode on search icon tap', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Initially should show search icon + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.byIcon(Icons.close), findsNothing); + expect(find.byType(TextField), findsNothing); + + // Tap search icon + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Should now show close icon and text field + expect(find.byIcon(Icons.close), findsOneWidget); + expect(find.byIcon(Icons.search), findsNothing); + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets('should exit search mode and clear query on close tap', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Enter search mode + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Enter some text + await tester.enterText(find.byType(TextField), 'test query'); + await tester.pump(); + + // Exit search mode + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + + // Should return to normal mode + expect(find.byIcon(Icons.search), findsOneWidget); + expect(find.byType(TextField), findsNothing); + expect(find.text('Add card'), findsOneWidget); + }); + + testWidgets('should update search query on text field change', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Enter search mode + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Enter text in search field + await tester.enterText(find.byType(TextField), 'visa'); + await tester.pump(); + + // Verify the text field contains the entered text + expect(find.text('visa'), findsOneWidget); + }); + + testWidgets('should display search hint text', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Enter search mode + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + // Check for hint text + expect(find.text('Search...'), findsOneWidget); + }); + + testWidgets('should have correct text field properties', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Enter search mode + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + + final textField = tester.widget(find.byType(TextField)); + expect(textField.autofocus, true); + expect(textField.decoration?.border, InputBorder.none); + expect(textField.decoration?.hintStyle?.color, Colors.white); + }); + + testWidgets('should create CardListBody with empty search query initially', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Verify CardListBody is created with empty search query + final cardListBody = tester.widget(find.byType(CardListBody)); + expect(cardListBody.searchQuery, ''); + }); + + testWidgets('should pass search query to CardListBody', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Enter search mode and add query + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'test'); + await tester.pump(); + + // Verify CardListBody receives the search query + final cardListBody = tester.widget(find.byType(CardListBody)); + expect(cardListBody.searchQuery, 'test'); + }); + + testWidgets('should toggle search mode multiple times', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + // Toggle search on + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + expect(find.byType(TextField), findsOneWidget); + + // Toggle search off + await tester.tap(find.byIcon(Icons.close)); + await tester.pump(); + expect(find.byType(TextField), findsNothing); + + // Toggle search on again + await tester.tap(find.byIcon(Icons.search)); + await tester.pump(); + expect(find.byType(TextField), findsOneWidget); + }); + + testWidgets('should maintain widget key', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp(home: CardList())); + + final cardList = tester.widget(find.byType(CardList)); + expect(cardList.key, isNotNull); + }); + }); + + group('CardListBody widget tests', () { + setUp(() { + // Mock the asset bundle + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage( + 'Visa\nMasterCard\nAmerican Express\nDiscover\n' + ); + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', null); + }); + + testWidgets('should display list of cards', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + // Wait for async data to load + await tester.pump(); + await tester.pump(); // Additional pump for FutureBuilder + + expect(find.byType(ListView), findsOneWidget); + expect(find.byType(ListTile), findsWidgets); + }); + + testWidgets('should display filtered cards based on search query', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: 'visa')), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Visa'), findsOneWidget); + expect(find.text('MasterCard'), findsNothing); + }); + + testWidgets('should display empty state when no cards match', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: 'nonexistent')), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Žádné karty'), findsOneWidget); + expect(find.byType(ListView), findsNothing); + }); + + testWidgets('should handle asset loading error', (WidgetTester tester) async { + // Mock asset loading failure + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + throw Exception('Asset not found'); + }); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.textContaining('Chyba:'), findsOneWidget); + }); + + testWidgets('should navigate to Scanner on card tap', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: const Scaffold(body: CardListBody()), + routes: { + '/scanner': (context) => const Scanner(cardName: 'test'), + }, + )); + + await tester.pump(); + await tester.pump(); + + // Find and tap the first card + final firstCard = find.byType(ListTile).first; + expect(firstCard, findsOneWidget); + + await tester.tap(firstCard); + await tester.pumpAndSettle(); + + // Verify navigation occurred (Scanner widget should be present) + expect(find.byType(Scanner), findsOneWidget); + }); + + testWidgets('should update cards when search query changes', (WidgetTester tester) async { + // Create a stateful widget to test didUpdateWidget + String searchQuery = ''; + late StateSetter setStateCallback; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (context, setState) { + setStateCallback = setState; + return CardListBody(searchQuery: searchQuery); + }, + ), + ), + )); + + await tester.pump(); + await tester.pump(); + + // Initially should show all cards + expect(find.byType(ListTile), findsNWidgets(4)); + + // Update search query + setStateCallback(() { + searchQuery = 'visa'; + }); + await tester.pump(); + await tester.pump(); + + // Should now show only filtered results + expect(find.text('Visa'), findsOneWidget); + expect(find.text('MasterCard'), findsNothing); + }); + + testWidgets('should display dividers between list items', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.byType(Divider), findsWidgets); + }); + + testWidgets('should handle empty asset file', (WidgetTester tester) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage(''); + } + return null; + }); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Žádné karty'), findsOneWidget); + }); + + testWidgets('should maintain search query state correctly', (WidgetTester tester) async { + const testQuery = 'american'; + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: testQuery)), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('American Express'), findsOneWidget); + expect(find.text('Visa'), findsNothing); + expect(find.text('MasterCard'), findsNothing); + }); + + testWidgets('should handle loading state', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + // Only first pump - should still be loading + await tester.pump(); + + // Should show CircularProgressIndicator or empty state while loading + // Flutter's FutureBuilder shows nothing while loading by default + expect(find.byType(ListView), findsNothing); + expect(find.text('Žádné karty'), findsNothing); + }); + + testWidgets('should pass correct card name to Scanner', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: const Scaffold(body: CardListBody()), + onGenerateRoute: (settings) { + if (settings.name == '/scanner') { + final args = settings.arguments as Map?; + return MaterialPageRoute( + builder: (context) => Scanner(cardName: args?['cardName'] ?? 'default'), + ); + } + return null; + }, + )); + + await tester.pump(); + await tester.pump(); + + // Tap on Visa card + await tester.tap(find.text('Visa')); + await tester.pumpAndSettle(); + + // Verify Scanner received correct card name + final scanner = tester.widget(find.byType(Scanner)); + expect(scanner.cardName, 'Visa'); + }); + + testWidgets('should handle different search query types', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: 'VISA')), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Visa'), findsOneWidget); + expect(find.byType(ListTile), findsOneWidget); + }); + + testWidgets('should maintain widget key', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + final cardListBody = tester.widget(find.byType(CardListBody)); + expect(cardListBody.key, isNotNull); + }); + + testWidgets('should not reload data when search query remains same', (WidgetTester tester) async { + const testQuery = 'visa'; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: StatefulBuilder( + builder: (context, setState) => const CardListBody(searchQuery: testQuery), + ), + ), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Visa'), findsOneWidget); + + // Rebuild with same search query + await tester.pump(); + + // Should still show same results without additional loading + expect(find.text('Visa'), findsOneWidget); + }); + }); + + group('CardListBody lifecycle tests', () { + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage('Visa\nMasterCard\n'); + } + return null; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', null); + }); + + testWidgets('should initialize with correct search query', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: 'visa')), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Visa'), findsOneWidget); + expect(find.text('MasterCard'), findsNothing); + }); + + testWidgets('should handle empty search query gracefully', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody(searchQuery: '')), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.byType(ListTile), findsNWidgets(2)); + }); + + testWidgets('should handle default constructor parameters', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + // Should work with default empty search query + expect(find.byType(ListTile), findsNWidgets(2)); + }); + + testWidgets('should properly dispose resources', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + // Remove the widget + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: Container()), + )); + + // Should not throw any errors during disposal + expect(tester.takeException(), isNull); + }); + }); + + group('Error handling and edge cases', () { + testWidgets('should handle malformed asset data', (WidgetTester tester) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + // Return malformed data + return const StandardMessageCodec().encodeMessage('\n\n\n'); + } + return null; + }); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Žádné karty'), findsOneWidget); + }); + + testWidgets('should handle very long card names', (WidgetTester tester) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage( + 'Very Long Card Company Name That Might Cause Display Issues\n' + ); + } + return null; + }); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.textContaining('Very Long Card Company Name'), findsOneWidget); + }); + + testWidgets('should handle unicode characters in card names', (WidgetTester tester) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + return const StandardMessageCodec().encodeMessage('Visá\nMasterCård\n中国银联\n'); + } + return null; + }); + + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: CardListBody()), + )); + + await tester.pump(); + await tester.pump(); + + expect(find.text('Visá'), findsOneWidget); + expect(find.text('MasterCård'), findsOneWidget); + expect(find.text('中国银联'), findsOneWidget); + }); + + test('should handle concurrent loadCards calls', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler('flutter/assets', (message) async { + final String key = const StandardMessageCodec().decodeMessage(message!) as String; + if (key == 'assets/cardCompanies') { + // Simulate slow loading + await Future.delayed(const Duration(milliseconds: 100)); + return const StandardMessageCodec().encodeMessage('Visa\nMasterCard\n'); + } + return null; + }); + + // Make concurrent calls + final futures = [ + loadCards('visa'), + loadCards('master'), + loadCards(''), + ]; + + final results = await Future.wait(futures); + + expect(results[0], hasLength(1)); + expect(results[1], hasLength(1)); + expect(results[2], hasLength(2)); + }); + }); +} \ No newline at end of file diff --git a/test/card/generate_mocks.dart b/test/card/generate_mocks.dart new file mode 100644 index 0000000..0bb4ba2 --- /dev/null +++ b/test/card/generate_mocks.dart @@ -0,0 +1,6 @@ +// Run this file to generate mocks: dart run build_runner build +import 'package:mockito/annotations.dart'; +import 'package:stashcard/providers/db.dart'; + +@GenerateMocks([DatabaseHelper]) +void main() {} \ No newline at end of file diff --git a/test/destination_test.dart b/test/destination_test.dart new file mode 100644 index 0000000..91895a6 --- /dev/null +++ b/test/destination_test.dart @@ -0,0 +1,441 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stashcard/Views/settings.dart'; +import 'package:stashcard/Views/home.dart'; +import '../lib/destination.dart'; // Adjust import path as needed + +void main() { + group('Destination', () { + testWidgets('should create Destination with all required properties', (WidgetTester tester) async { + // Arrange + const index = 0; + const title = 'Home'; + const icon = Icons.home; + const selectedIcon = Icons.home_filled; + + // Act + const destination = Destination(index, title, icon, selectedIcon); + + // Assert + expect(destination.index, equals(index)); + expect(destination.title, equals(title)); + expect(destination.icon, equals(icon)); + expect(destination.selectedIcon, equals(selectedIcon)); + }); + + test('should create Destination with different index values', () { + // Test different index values + const destinations = [ + Destination(0, 'Home', Icons.home, Icons.home_filled), + Destination(1, 'Settings', Icons.settings, Icons.settings_filled), + Destination(2, 'Profile', Icons.person, Icons.person_filled), + Destination(-1, 'Invalid', Icons.error, Icons.error_outline), + ]; + + expect(destinations[0].index, equals(0)); + expect(destinations[1].index, equals(1)); + expect(destinations[2].index, equals(2)); + expect(destinations[3].index, equals(-1)); + }); + + test('should handle empty and special character titles', () { + const destinations = [ + Destination(0, '', Icons.home, Icons.home_filled), + Destination(1, 'Title with spaces', Icons.settings, Icons.settings_filled), + Destination(2, 'Title_with_underscores', Icons.person, Icons.person_filled), + Destination(3, 'Title-with-dashes', Icons.star, Icons.star_filled), + Destination(4, 'Title with émojis 🏠', Icons.emoji_emotions, Icons.emoji_emotions_outlined), + ]; + + expect(destinations[0].title, equals('')); + expect(destinations[1].title, equals('Title with spaces')); + expect(destinations[2].title, equals('Title_with_underscores')); + expect(destinations[3].title, equals('Title-with-dashes')); + expect(destinations[4].title, equals('Title with émojis 🏠')); + }); + + test('should handle different icon combinations', () { + const destination1 = Destination(0, 'Same Icons', Icons.home, Icons.home); + const destination2 = Destination(1, 'Different Icons', Icons.home, Icons.home_filled); + const destination3 = Destination(2, 'Custom Icons', Icons.star, Icons.favorite); + + expect(destination1.icon, equals(destination1.selectedIcon)); + expect(destination2.icon, isNot(equals(destination2.selectedIcon))); + expect(destination3.icon, equals(Icons.star)); + expect(destination3.selectedIcon, equals(Icons.favorite)); + }); + }); + + group('DestinationView', () { + testWidgets('should create DestinationView with required parameters', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + const widget = DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ); + + // Assert + expect(widget.destination, equals(destination)); + expect(widget.navigatorKey, equals(navigatorKey)); + }); + + testWidgets('should build Navigator with correct key', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + final navigator = tester.widget(find.byType(Navigator)); + expect(navigator.key, equals(navigatorKey)); + }); + + testWidgets('should navigate to Home when destination index is 0', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets('should navigate to SettingsPage when destination index is 1', (WidgetTester tester) async { + // Arrange + const destination = Destination(1, 'Settings', Icons.settings, Icons.settings_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(Home), findsNothing); + }); + + testWidgets('should return SizedBox for invalid destination index', (WidgetTester tester) async { + // Arrange + const destination = Destination(2, 'Invalid', Icons.error, Icons.error_outline); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(SizedBox), findsOneWidget); + expect(find.byType(Home), findsNothing); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets('should return SizedBox for negative destination index', (WidgetTester tester) async { + // Arrange + const destination = Destination(-1, 'Negative', Icons.error, Icons.error_outline); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(SizedBox), findsOneWidget); + expect(find.byType(Home), findsNothing); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets('should handle route settings correctly', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + final navigator = tester.widget(find.byType(Navigator)); + expect(navigator.onGenerateRoute, isNotNull); + }); + + testWidgets('should create MaterialPageRoute with correct settings', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Act - trigger route generation + final navigator = tester.widget(find.byType(Navigator)); + const routeSettings = RouteSettings(name: '/'); + final route = navigator.onGenerateRoute!(routeSettings); + + // Assert + expect(route, isA()); + expect(route?.settings, equals(routeSettings)); + }); + + testWidgets('should handle null route settings gracefully', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Act - trigger route generation with null name + final navigator = tester.widget(find.byType(Navigator)); + const routeSettings = RouteSettings(name: null); + final route = navigator.onGenerateRoute!(routeSettings); + + // Assert + expect(route, isA()); + expect(route?.settings, equals(routeSettings)); + }); + + testWidgets('should handle custom route names', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Act - trigger route generation with custom name + final navigator = tester.widget(find.byType(Navigator)); + const routeSettings = RouteSettings(name: '/custom'); + final route = navigator.onGenerateRoute!(routeSettings); + + // Assert + expect(route, isA()); + final materialRoute = route as MaterialPageRoute; + final widget = materialRoute.builder(tester.element(find.byType(MaterialApp))); + expect(widget, isA()); + }); + + testWidgets('should maintain state across rebuilds', (WidgetTester tester) async { + // Arrange + const destination1 = Destination(0, 'Home', Icons.home, Icons.home_filled); + const destination2 = Destination(1, 'Settings', Icons.settings, Icons.settings_filled); + const navigatorKey = Key('test_navigator'); + + // Act - Initial build + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination1, + navigatorKey: navigatorKey, + ), + ), + ); + + expect(find.byType(Home), findsOneWidget); + + // Act - Rebuild with different destination + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination2, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(Home), findsNothing); + }); + + testWidgets('should work with different navigator keys', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home_filled); + const navigatorKey1 = Key('test_navigator_1'); + const navigatorKey2 = Key('test_navigator_2'); + + // Act & Assert - First navigator key + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey1, + ), + ), + ); + + var navigator = tester.widget(find.byType(Navigator)); + expect(navigator.key, equals(navigatorKey1)); + + // Act & Assert - Second navigator key + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey2, + ), + ), + ); + + navigator = tester.widget(find.byType(Navigator)); + expect(navigator.key, equals(navigatorKey2)); + }); + + testWidgets('should handle extreme index values', (WidgetTester tester) async { + // Test with very large positive index + const destination1 = Destination(999999, 'Large Index', Icons.error, Icons.error_outline); + const navigatorKey = Key('test_navigator'); + + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination1, + navigatorKey: navigatorKey, + ), + ), + ); + + expect(find.byType(SizedBox), findsOneWidget); + + // Test with very large negative index + const destination2 = Destination(-999999, 'Large Negative Index', Icons.error, Icons.error_outline); + + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination2, + navigatorKey: navigatorKey, + ), + ), + ); + + expect(find.byType(SizedBox), findsOneWidget); + }); + + group('Edge Cases', () { + testWidgets('should handle destination with same icon and selectedIcon', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, 'Home', Icons.home, Icons.home); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(Home), findsOneWidget); + expect(destination.icon, equals(destination.selectedIcon)); + }); + + testWidgets('should handle destination with empty title', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, '', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(Home), findsOneWidget); + expect(destination.title, equals('')); + }); + + testWidgets('should handle unicode characters in title', (WidgetTester tester) async { + // Arrange + const destination = Destination(0, '🏠 Hôme 测试', Icons.home, Icons.home_filled); + const navigatorKey = Key('test_navigator'); + + // Act + await tester.pumpWidget( + MaterialApp( + home: const DestinationView( + destination: destination, + navigatorKey: navigatorKey, + ), + ), + ); + + // Assert + expect(find.byType(Home), findsOneWidget); + expect(destination.title, equals('🏠 Hôme 测试')); + }); + }); + }); +} \ No newline at end of file diff --git a/test/main_test.dart b/test/main_test.dart new file mode 100644 index 0000000..1e305cc --- /dev/null +++ b/test/main_test.dart @@ -0,0 +1,589 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:stashcard/main.dart'; +import 'package:stashcard/providers/theme_provider.dart'; +import 'package:stashcard/Views/home.dart'; +import 'package:stashcard/Views/settings.dart'; +import 'package:stashcard/destination.dart'; + +// Mock ThemeProvider for testing +class MockThemeProvider extends ChangeNotifier implements ThemeProvider { + Color _seedColor = Colors.blue; + ThemeMode _mode = ThemeMode.system; + + @override + Color get seedColor => _seedColor; + + @override + ThemeData get lightTheme => ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: _seedColor, + brightness: Brightness.light, + ), + ); + + @override + ThemeData get darkTheme => ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: _seedColor, + brightness: Brightness.dark, + ), + ); + + @override + ThemeMode get themeMode => _mode; + + @override + void setSeedColor(Color color) { + _seedColor = color; + notifyListeners(); + } + + @override + void setThemeMode(ThemeMode mode) { + _mode = mode; + notifyListeners(); + } +} + +void main() { + group('StashcardApp Widget Tests', () { + testWidgets('StashcardApp builds correctly with ThemeProvider', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: const StashcardApp(), + ), + ); + + // Assert + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(NavigationHandler), findsOneWidget); + }); + + testWidgets('StashcardApp uses correct theme configuration', (WidgetTester tester) async { + // Arrange + final mockThemeProvider = MockThemeProvider(); + + // Act + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const StashcardApp(), + ), + ); + + // Assert + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.theme, equals(mockThemeProvider.lightTheme)); + expect(materialApp.darkTheme, equals(mockThemeProvider.darkTheme)); + expect(materialApp.themeMode, equals(mockThemeProvider.themeMode)); + expect(materialApp.home, isA()); + }); + + testWidgets('StashcardApp throws error when ThemeProvider is missing', (WidgetTester tester) async { + // Act & Assert + expect(() async { + await tester.pumpWidget(const StashcardApp()); + }, throwsA(isA())); + }); + + testWidgets('StashcardApp responds to theme changes', (WidgetTester tester) async { + // Arrange + final mockThemeProvider = MockThemeProvider(); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const StashcardApp(), + ), + ); + + // Act - Change theme + mockThemeProvider.setSeedColor(Colors.red); + await tester.pump(); + + // Assert + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.theme?.colorScheme.primary, + equals(ColorScheme.fromSeed(seedColor: Colors.red, brightness: Brightness.light).primary)); + }); + }); + + group('NavigationHandler Widget Tests', () { + Widget createTestableNavigationHandler() { + return ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp( + home: const NavigationHandler(), + ), + ); + } + + testWidgets('NavigationHandler builds with correct initial state', (WidgetTester tester) async { + // Act + await tester.pumpWidget(createTestableNavigationHandler()); + + // Assert + expect(find.byType(Scaffold), findsOneWidget); + expect(find.byType(NavigationBar), findsOneWidget); + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + + // Verify navigation destinations are present + expect(find.text('Home'), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + }); + + testWidgets('NavigationHandler has correct number of destinations', (WidgetTester tester) async { + // Act + await tester.pumpWidget(createTestableNavigationHandler()); + + // Assert + expect(find.byType(NavigationDestination), findsNWidgets(2)); + }); + + testWidgets('NavigationHandler navigation switches views correctly', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestableNavigationHandler()); + + // Assert initial state + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + + // Act - Navigate to Settings + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + + // Assert Settings view is shown + expect(find.byType(Home), findsNothing); + expect(find.byType(SettingsPage), findsOneWidget); + + // Act - Navigate back to Home + await tester.tap(find.text('Home')); + await tester.pumpAndSettle(); + + // Assert Home view is shown again + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets('NavigationHandler selectedIndex updates correctly', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestableNavigationHandler()); + + // Assert initial selectedIndex + var navigationBar = tester.widget(find.byType(NavigationBar)); + expect(navigationBar.selectedIndex, equals(0)); + + // Act - Navigate to Settings + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + + // Assert selectedIndex updated + navigationBar = tester.widget(find.byType(NavigationBar)); + expect(navigationBar.selectedIndex, equals(1)); + + // Act - Navigate back to Home + await tester.tap(find.text('Home')); + await tester.pumpAndSettle(); + + // Assert selectedIndex back to 0 + navigationBar = tester.widget(find.byType(NavigationBar)); + expect(navigationBar.selectedIndex, equals(0)); + }); + + testWidgets('NavigationHandler destinations have correct icons and labels', (WidgetTester tester) async { + // Act + await tester.pumpWidget(createTestableNavigationHandler()); + + // Assert + final destinations = tester.widgetList(find.byType(NavigationDestination)).toList(); + + // Verify Home destination + final homeDestination = destinations[0]; + expect((homeDestination.icon as Icon).icon, equals(Icons.home_outlined)); + expect((homeDestination.selectedIcon as Icon).icon, equals(Icons.home)); + expect(homeDestination.label, equals('Home')); + + // Verify Settings destination + final settingsDestination = destinations[1]; + expect((settingsDestination.icon as Icon).icon, equals(Icons.settings_outlined)); + expect((settingsDestination.selectedIcon as Icon).icon, equals(Icons.settings)); + expect(settingsDestination.label, equals('Settings')); + }); + + testWidgets('NavigationHandler maintains correct route-destination mapping', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget(createTestableNavigationHandler()); + + // Get the state to verify internal structure + final state = tester.state<_NavigationHandlerState>(find.byType(NavigationHandler)); + + // Assert destinations and routes alignment + expect(state.destinations.length, equals(state.routes.length)); + expect(state.destinations.length, equals(2)); + + // Verify destination properties + expect(state.destinations[0].index, equals(0)); + expect(state.destinations[0].title, equals('Home')); + expect(state.destinations[1].index, equals(1)); + expect(state.destinations[1].title, equals('Settings')); + + // Verify routes + expect(state.routes[0], isA()); + expect(state.routes[1], isA()); + }); + }); + + group('_NavigationHandlerState Internal Tests', () { + testWidgets('_changeDestination method updates state correctly', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + final state = tester.state<_NavigationHandlerState>(find.byType(NavigationHandler)); + + // Assert initial state + expect(state._selectedIndex, equals(0)); + + // Act - Simulate navigation change through UI + await tester.tap(find.text('Settings')); + await tester.pump(); + + // Assert state changed + expect(state._selectedIndex, equals(1)); + }); + + testWidgets('routes list contains correct widget instances', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + final state = tester.state<_NavigationHandlerState>(find.byType(NavigationHandler)); + + // Assert routes configuration + expect(state.routes.length, equals(2)); + expect(state.routes[0].runtimeType, equals(Home)); + expect(state.routes[1].runtimeType, equals(SettingsPage)); + }); + + testWidgets('destinations list is properly configured', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + final state = tester.state<_NavigationHandlerState>(find.byType(NavigationHandler)); + + // Assert destinations configuration + expect(state.destinations.length, equals(2)); + + final homeDestination = state.destinations[0]; + expect(homeDestination.index, equals(0)); + expect(homeDestination.title, equals('Home')); + expect(homeDestination.icon, equals(Icons.home_outlined)); + expect(homeDestination.selectedIcon, equals(Icons.home)); + + final settingsDestination = state.destinations[1]; + expect(settingsDestination.index, equals(1)); + expect(settingsDestination.title, equals('Settings')); + expect(settingsDestination.icon, equals(Icons.settings_outlined)); + expect(settingsDestination.selectedIcon, equals(Icons.settings)); + }); + }); + + group('Integration Tests', () { + testWidgets('Complete app flow works end-to-end', (WidgetTester tester) async { + // Arrange & Act + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: const StashcardApp(), + ), + ); + + // Assert initial app state + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(NavigationHandler), findsOneWidget); + expect(find.byType(Home), findsOneWidget); + expect(find.byType(NavigationBar), findsOneWidget); + + // Act - Test complete navigation flow + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(Home), findsNothing); + + await tester.tap(find.text('Home')); + await tester.pumpAndSettle(); + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + }); + + testWidgets('App handles rapid navigation changes', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + // Act - Perform rapid navigation switches + for (int i = 0; i < 5; i++) { + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.byType(SettingsPage), findsOneWidget); + + await tester.tap(find.text('Home')); + await tester.pumpAndSettle(); + expect(find.byType(Home), findsOneWidget); + } + }); + + testWidgets('App maintains consistent state during theme changes', (WidgetTester tester) async { + // Arrange + final themeProvider = MockThemeProvider(); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: themeProvider, + child: const StashcardApp(), + ), + ); + + // Navigate to Settings + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + expect(find.byType(SettingsPage), findsOneWidget); + + // Act - Change theme while on Settings page + themeProvider.setThemeMode(ThemeMode.dark); + await tester.pump(); + + // Assert - Should still be on Settings page + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.byType(Home), findsNothing); + + final navigationBar = tester.widget(find.byType(NavigationBar)); + expect(navigationBar.selectedIndex, equals(1)); + }); + }); + + group('Edge Case Tests', () { + testWidgets('NavigationHandler handles widget rebuilds gracefully', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + // Navigate to Settings + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + + // Act - Force multiple rebuilds + for (int i = 0; i < 3; i++) { + await tester.pump(); + } + + // Assert - Should maintain Settings state + expect(find.byType(SettingsPage), findsOneWidget); + final navigationBar = tester.widget(find.byType(NavigationBar)); + expect(navigationBar.selectedIndex, equals(1)); + }); + + testWidgets('App handles provider updates during navigation', (WidgetTester tester) async { + // Arrange + final themeProvider = MockThemeProvider(); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: themeProvider, + child: const StashcardApp(), + ), + ); + + // Act - Change theme and navigate simultaneously + themeProvider.setSeedColor(Colors.purple); + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + + // Assert - Both operations should succeed + expect(find.byType(SettingsPage), findsOneWidget); + final materialApp = tester.widget(find.byType(MaterialApp)); + expect(materialApp.theme?.colorScheme.primary, + equals(ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light).primary)); + }); + + testWidgets('NavigationHandler maintains state consistency', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + final state = tester.state<_NavigationHandlerState>(find.byType(NavigationHandler)); + + // Act & Assert - Verify state consistency after multiple operations + expect(state._selectedIndex, equals(0)); + expect(state.destinations.length, equals(2)); + expect(state.routes.length, equals(2)); + + // Navigate and verify state + await tester.tap(find.text('Settings')); + await tester.pump(); + expect(state._selectedIndex, equals(1)); + + // Return and verify state + await tester.tap(find.text('Home')); + await tester.pump(); + expect(state._selectedIndex, equals(0)); + }); + }); + + group('Performance and Memory Tests', () { + testWidgets('NavigationHandler does not leak widgets during navigation', (WidgetTester tester) async { + // Arrange + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + // Act - Navigate multiple times to test for widget leaks + for (int i = 0; i < 10; i++) { + await tester.tap(find.text('Settings')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Home')); + await tester.pumpAndSettle(); + } + + // Assert - Should still have exactly one of each widget type + expect(find.byType(Home), findsOneWidget); + expect(find.byType(SettingsPage), findsNothing); + expect(find.byType(NavigationBar), findsOneWidget); + expect(find.byType(Scaffold), findsOneWidget); + }); + + testWidgets('App handles multiple provider notifications efficiently', (WidgetTester tester) async { + // Arrange + final themeProvider = MockThemeProvider(); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: themeProvider, + child: const StashcardApp(), + ), + ); + + // Act - Trigger multiple rapid provider updates + for (int i = 0; i < 5; i++) { + themeProvider.setSeedColor(Color(0xFF000000 + i * 0x111111)); + await tester.pump(const Duration(milliseconds: 10)); + } + + // Assert - App should still be functional + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(NavigationHandler), findsOneWidget); + expect(find.byType(Home), findsOneWidget); + }); + }); + + group('Destination Class Tests', () { + test('Destination class creates instances with correct properties', () { + // Arrange & Act + const destination = Destination(0, "Test", Icons.star, Icons.star_filled); + + // Assert + expect(destination.index, equals(0)); + expect(destination.title, equals("Test")); + expect(destination.icon, equals(Icons.star)); + expect(destination.selectedIcon, equals(Icons.star_filled)); + }); + + test('Destination class handles different icon types', () { + // Arrange & Act + const homeDestination = Destination(0, "Home", Icons.home_outlined, Icons.home); + const settingsDestination = Destination(1, "Settings", Icons.settings_outlined, Icons.settings); + + // Assert + expect(homeDestination.icon, equals(Icons.home_outlined)); + expect(homeDestination.selectedIcon, equals(Icons.home)); + expect(settingsDestination.icon, equals(Icons.settings_outlined)); + expect(settingsDestination.selectedIcon, equals(Icons.settings)); + }); + }); + + group('Main Function Tests', () { + testWidgets('main function initializes app correctly', (WidgetTester tester) async { + // Note: Testing main() directly is challenging in widget tests + // Instead, we test the structure it creates + + // Act - Create the same structure as main() + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: const StashcardApp(), + ), + ); + + // Assert - Verify the expected widget tree structure + expect(find.byType(ChangeNotifierProvider), findsOneWidget); + expect(find.byType(StashcardApp), findsOneWidget); + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(NavigationHandler), findsOneWidget); + }); + }); + + group('Constructor Tests', () { + testWidgets('StashcardApp constructor works with key parameter', (WidgetTester tester) async { + // Arrange + const testKey = Key('test_stashcard_app'); + + // Act + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: const StashcardApp(key: testKey), + ), + ); + + // Assert + expect(find.byKey(testKey), findsOneWidget); + }); + + testWidgets('NavigationHandler constructor works with default parameters', (WidgetTester tester) async { + // Act + await tester.pumpWidget( + ChangeNotifierProvider( + create: (context) => MockThemeProvider(), + child: MaterialApp(home: const NavigationHandler()), + ), + ); + + // Assert - Should build without issues + expect(find.byType(NavigationHandler), findsOneWidget); + expect(find.byType(Scaffold), findsOneWidget); + }); + }); +} \ No newline at end of file diff --git a/test/providers/db_test.dart b/test/providers/db_test.dart new file mode 100644 index 0000000..44bd786 --- /dev/null +++ b/test/providers/db_test.dart @@ -0,0 +1,748 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:path/path.dart'; + +import '../../lib/providers/db.dart'; +import '../../lib/Views/home.dart'; + +void main() { + // Initialize FFI for testing + setUpAll(() { + sqfliteFfiInit(); + databaseFactory = databaseFactoryFfi; + }); + + group('UserCard Model Tests', () { + late DateTime testDate; + late UserCard testCard; + + setUp(() { + testDate = DateTime(2024, 1, 15, 10, 30, 0); + testCard = UserCard( + id: 1, + name: 'Test Card', + code: '123456789', + usage: 5, + createdAt: testDate, + symbology: 'CODE128', + ); + }); + + test('should create UserCard with all required fields', () { + expect(testCard.id, equals(1)); + expect(testCard.name, equals('Test Card')); + expect(testCard.code, equals('123456789')); + expect(testCard.usage, equals(5)); + expect(testCard.createdAt, equals(testDate)); + expect(testCard.symbology, equals('CODE128')); + }); + + test('should create UserCard without id (nullable)', () { + final cardWithoutId = UserCard( + name: 'Test Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + expect(cardWithoutId.id, isNull); + expect(cardWithoutId.name, equals('Test Card')); + }); + + test('toMap() should convert UserCard to correct map format', () { + final map = testCard.toMap(); + + expect(map['id'], equals(1)); + expect(map['name'], equals('Test Card')); + expect(map['code'], equals('123456789')); + expect(map['usage'], equals(5)); + expect(map['created_at'], equals(testDate.toIso8601String())); + expect(map['symbology'], equals('CODE128')); + }); + + test('toMap() should handle null id correctly', () { + final cardWithoutId = UserCard( + name: 'Test Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + final map = cardWithoutId.toMap(); + expect(map['id'], isNull); + }); + + test('toString() should return formatted string representation', () { + final stringRep = testCard.toString(); + + expect(stringRep, contains('UserCard{')); + expect(stringRep, contains('id: 1')); + expect(stringRep, contains('name: Test Card')); + expect(stringRep, contains('code: 123456789')); + expect(stringRep, contains('usage 5')); + expect(stringRep, contains('created_at: $testDate')); + expect(stringRep, contains('symbology: CODE128')); + }); + + test('copyWith() should create new instance with updated values', () { + final updatedCard = testCard.copyWith( + name: 'Updated Card', + usage: 10, + ); + + expect(updatedCard.id, equals(1)); + expect(updatedCard.name, equals('Updated Card')); + expect(updatedCard.code, equals('123456789')); + expect(updatedCard.usage, equals(10)); + expect(updatedCard.createdAt, equals(testDate)); + expect(updatedCard.symbology, equals('CODE128')); + }); + + test('copyWith() should preserve original values when no parameters provided', () { + final copiedCard = testCard.copyWith(); + + expect(copiedCard.id, equals(testCard.id)); + expect(copiedCard.name, equals(testCard.name)); + expect(copiedCard.code, equals(testCard.code)); + expect(copiedCard.usage, equals(testCard.usage)); + expect(copiedCard.createdAt, equals(testCard.createdAt)); + expect(copiedCard.symbology, equals(testCard.symbology)); + }); + + test('copyWith() should update all fields when all parameters provided', () { + final newDate = DateTime(2024, 2, 20, 15, 45, 0); + final newCard = testCard.copyWith( + id: 99, + name: 'Completely New Card', + code: '987654321', + usage: 25, + createdAt: newDate, + symbology: 'QR_CODE', + ); + + expect(newCard.id, equals(99)); + expect(newCard.name, equals('Completely New Card')); + expect(newCard.code, equals('987654321')); + expect(newCard.usage, equals(25)); + expect(newCard.createdAt, equals(newDate)); + expect(newCard.symbology, equals('QR_CODE')); + }); + + test('should handle edge cases for string fields', () { + final edgeCaseCard = UserCard( + name: '', + code: '', + usage: 0, + createdAt: testDate, + symbology: '', + ); + + expect(edgeCaseCard.name, equals('')); + expect(edgeCaseCard.code, equals('')); + expect(edgeCaseCard.symbology, equals('')); + }); + + test('should handle large usage numbers', () { + final largeUsageCard = UserCard( + name: 'High Usage Card', + code: '123456789', + usage: 999999, + createdAt: testDate, + symbology: 'CODE128', + ); + + expect(largeUsageCard.usage, equals(999999)); + }); + + test('should handle special characters in fields', () { + final specialCard = UserCard( + name: 'Special Card áéíóú ñ 中文', + code: '!@#$%^&*()', + usage: 0, + createdAt: testDate, + symbology: 'EAN-13', + ); + + expect(specialCard.name, equals('Special Card áéíóú ñ 中文')); + expect(specialCard.code, equals('!@#$%^&*()')); + expect(specialCard.symbology, equals('EAN-13')); + }); + + test('should handle negative usage numbers', () { + final negativeUsageCard = UserCard( + name: 'Negative Usage Card', + code: '123456789', + usage: -5, + createdAt: testDate, + symbology: 'CODE128', + ); + + expect(negativeUsageCard.usage, equals(-5)); + }); + + test('should handle zero usage', () { + final zeroUsageCard = UserCard( + name: 'Zero Usage Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + expect(zeroUsageCard.usage, equals(0)); + }); + + test('should handle DateTime edge cases', () { + final futureDate = DateTime(2099, 12, 31, 23, 59, 59); + final pastDate = DateTime(1900, 1, 1, 0, 0, 0); + + final futureCard = testCard.copyWith(createdAt: futureDate); + final pastCard = testCard.copyWith(createdAt: pastDate); + + expect(futureCard.createdAt, equals(futureDate)); + expect(pastCard.createdAt, equals(pastDate)); + }); + }); + + group('DatabaseHelper Tests', () { + late DatabaseHelper dbHelper; + late DateTime testDate; + + setUp(() async { + dbHelper = DatabaseHelper(); + testDate = DateTime(2024, 1, 15, 10, 30, 0); + + // Clean up any existing data + try { + await dbHelper.deleteAllCards(); + } catch (e) { + // Database might not exist yet, ignore error + } + }); + + tearDown(() async { + // Clean up after each test + try { + await dbHelper.deleteAllCards(); + } catch (e) { + // Ignore cleanup errors + } + }); + + test('should get database instance', () async { + final db = await dbHelper.database; + expect(db, isNotNull); + expect(db.isOpen, isTrue); + }); + + test('should return same database instance on subsequent calls', () async { + final db1 = await dbHelper.database; + final db2 = await dbHelper.database; + expect(identical(db1, db2), isTrue); + }); + + test('should insert card successfully', () async { + final card = UserCard( + name: 'Test Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + + expect(cards.length, equals(1)); + expect(cards[0].name, equals('Test Card')); + expect(cards[0].code, equals('123456789')); + }); + + test('should replace card on conflict', () async { + final card1 = UserCard( + id: 1, + name: 'Original Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + final card2 = UserCard( + id: 1, + name: 'Updated Card', + code: '987654321', + usage: 5, + createdAt: testDate, + symbology: 'QR_CODE', + ); + + await dbHelper.insertCard(card1); + await dbHelper.insertCard(card2); + + final cards = await dbHelper.getUserCards(); + expect(cards.length, equals(1)); + expect(cards[0].name, equals('Updated Card')); + expect(cards[0].code, equals('987654321')); + }); + + test('should retrieve all user cards', () async { + final cards = [ + UserCard(name: 'Card 1', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Card 2', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'Card 3', code: '333', usage: 3, createdAt: testDate, symbology: 'EAN13'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final retrievedCards = await dbHelper.getUserCards(); + expect(retrievedCards.length, equals(3)); + }); + + test('should return empty list when no cards exist', () async { + final cards = await dbHelper.getUserCards(); + expect(cards, isEmpty); + }); + + test('should get cards sorted by name ascending', () async { + final cards = [ + UserCard(name: 'Zebra Card', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Alpha Card', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'Beta Card', code: '333', usage: 3, createdAt: testDate, symbology: 'EAN13'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final sortedCards = await dbHelper.getUserCardsSorted(SortOptions.byName); + expect(sortedCards[0].name, equals('Alpha Card')); + expect(sortedCards[1].name, equals('Beta Card')); + expect(sortedCards[2].name, equals('Zebra Card')); + }); + + test('should get cards sorted by usage descending', () async { + final cards = [ + UserCard(name: 'Low Usage', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'High Usage', code: '222', usage: 10, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'Medium Usage', code: '333', usage: 5, createdAt: testDate, symbology: 'EAN13'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final sortedCards = await dbHelper.getUserCardsSorted(SortOptions.byUsage); + expect(sortedCards[0].name, equals('High Usage')); + expect(sortedCards[1].name, equals('Medium Usage')); + expect(sortedCards[2].name, equals('Low Usage')); + }); + + test('should get cards sorted by date created descending', () async { + final date1 = DateTime(2024, 1, 1); + final date2 = DateTime(2024, 1, 2); + final date3 = DateTime(2024, 1, 3); + + final cards = [ + UserCard(name: 'Oldest', code: '111', usage: 1, createdAt: date1, symbology: 'CODE128'), + UserCard(name: 'Newest', code: '222', usage: 2, createdAt: date3, symbology: 'QR_CODE'), + UserCard(name: 'Middle', code: '333', usage: 3, createdAt: date2, symbology: 'EAN13'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final sortedCards = await dbHelper.getUserCardsSorted(SortOptions.byDateCreated); + expect(sortedCards[0].name, equals('Newest')); + expect(sortedCards[1].name, equals('Middle')); + expect(sortedCards[2].name, equals('Oldest')); + }); + + test('should retrieve one card by id', () async { + final card = UserCard( + name: 'Specific Card', + code: '123456789', + usage: 5, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + final retrievedCard = await dbHelper.getOneCard(cardId); + expect(retrievedCard.name, equals('Specific Card')); + expect(retrievedCard.id, equals(cardId)); + }); + + test('getOneCard should throw when card does not exist', () async { + expect(() => dbHelper.getOneCard(999), throwsA(isA())); + }); + + test('should get last added card', () async { + final cards = [ + UserCard(name: 'First Card', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Second Card', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'Last Card', code: '333', usage: 3, createdAt: testDate, symbology: 'EAN13'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final lastCard = await dbHelper.getLastAddedCard(); + expect(lastCard.name, equals('Last Card')); + }); + + test('getLastAddedCard should throw when no cards exist', () async { + expect(() => dbHelper.getLastAddedCard(), throwsA(isA())); + }); + + test('should increment usage count', () async { + final card = UserCard( + name: 'Usage Card', + code: '123456789', + usage: 5, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + await dbHelper.incrementUsage(cardId); + + final updatedCard = await dbHelper.getOneCard(cardId); + expect(updatedCard.usage, equals(6)); + }); + + test('should increment usage from zero', () async { + final card = UserCard( + name: 'Zero Usage Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + await dbHelper.incrementUsage(cardId); + + final updatedCard = await dbHelper.getOneCard(cardId); + expect(updatedCard.usage, equals(1)); + }); + + test('should handle incrementing usage for non-existent card', () async { + expect(() => dbHelper.incrementUsage(999), throwsA(isA())); + }); + + test('should update user card', () async { + final originalCard = UserCard( + name: 'Original Name', + code: '123456789', + usage: 5, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(originalCard); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + final updatedCard = UserCard( + id: cardId, + name: 'Updated Name', + code: '987654321', + usage: 10, + createdAt: testDate, + symbology: 'QR_CODE', + ); + + await dbHelper.updateUserCard(updatedCard); + + final retrievedCard = await dbHelper.getOneCard(cardId); + expect(retrievedCard.name, equals('Updated Name')); + expect(retrievedCard.code, equals('987654321')); + expect(retrievedCard.usage, equals(10)); + expect(retrievedCard.symbology, equals('QR_CODE')); + }); + + test('should delete all cards', () async { + final cards = [ + UserCard(name: 'Card 1', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Card 2', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + expect((await dbHelper.getUserCards()).length, equals(2)); + + await dbHelper.deleteAllCards(); + + final remainingCards = await dbHelper.getUserCards(); + expect(remainingCards, isEmpty); + }); + + test('should delete specific user card', () async { + final cards = [ + UserCard(name: 'Keep Card', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Delete Card', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + ]; + + for (final card in cards) { + await dbHelper.insertCard(card); + } + + final allCards = await dbHelper.getUserCards(); + final cardToDelete = allCards.firstWhere((card) => card.name == 'Delete Card'); + + await dbHelper.deleteUserCard(cardToDelete.id!); + + final remainingCards = await dbHelper.getUserCards(); + expect(remainingCards.length, equals(1)); + expect(remainingCards[0].name, equals('Keep Card')); + }); + + test('should handle deleting non-existent card gracefully', () async { + // This should not throw an exception + await dbHelper.deleteUserCard(999); + + final cards = await dbHelper.getUserCards(); + expect(cards, isEmpty); + }); + + test('should handle special characters in card data', () async { + final specialCard = UserCard( + name: 'Special Card áéíóú ñ 中文', + code: '!@#$%^&*()', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(specialCard); + final cards = await dbHelper.getUserCards(); + + expect(cards[0].name, equals('Special Card áéíóú ñ 中文')); + expect(cards[0].code, equals('!@#$%^&*()')); + }); + + test('should handle very long strings', () async { + final longString = 'A' * 1000; + final longCard = UserCard( + name: longString, + code: longString, + usage: 0, + createdAt: testDate, + symbology: longString, + ); + + await dbHelper.insertCard(longCard); + final cards = await dbHelper.getUserCards(); + + expect(cards[0].name, equals(longString)); + expect(cards[0].code, equals(longString)); + expect(cards[0].symbology, equals(longString)); + }); + + test('should preserve DateTime precision when storing and retrieving', () async { + final preciseDate = DateTime(2024, 1, 15, 10, 30, 45, 123, 456); + final card = UserCard( + name: 'Precise Date Card', + code: '123456789', + usage: 0, + createdAt: preciseDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + + // Note: Database storage may lose some precision, but should preserve at least milliseconds + expect(cards[0].createdAt.year, equals(preciseDate.year)); + expect(cards[0].createdAt.month, equals(preciseDate.month)); + expect(cards[0].createdAt.day, equals(preciseDate.day)); + expect(cards[0].createdAt.hour, equals(preciseDate.hour)); + expect(cards[0].createdAt.minute, equals(preciseDate.minute)); + expect(cards[0].createdAt.second, equals(preciseDate.second)); + }); + + test('should handle maximum integer values for usage', () async { + const maxUsage = 9223372036854775807; // Max int64 value + final maxUsageCard = UserCard( + name: 'Max Usage Card', + code: '123456789', + usage: maxUsage, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(maxUsageCard); + final cards = await dbHelper.getUserCards(); + + expect(cards[0].usage, equals(maxUsage)); + }); + + test('should handle concurrent database operations', () async { + final futures = []; + + // Create multiple concurrent insert operations + for (int i = 0; i < 10; i++) { + final card = UserCard( + name: 'Concurrent Card $i', + code: 'CODE$i', + usage: i, + createdAt: testDate, + symbology: 'CODE128', + ); + futures.add(dbHelper.insertCard(card)); + } + + await Future.wait(futures); + + final cards = await dbHelper.getUserCards(); + expect(cards.length, equals(10)); + }); + + test('should handle empty sort results', () async { + final sortedCards = await dbHelper.getUserCardsSorted(SortOptions.byName); + expect(sortedCards, isEmpty); + }); + + test('should maintain data integrity with multiple updates', () async { + final card = UserCard( + name: 'Test Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + // Multiple increments + for (int i = 0; i < 5; i++) { + await dbHelper.incrementUsage(cardId); + } + + final finalCard = await dbHelper.getOneCard(cardId); + expect(finalCard.usage, equals(5)); + }); + + test('should handle null values in database properly', () async { + final cardWithoutId = UserCard( + name: 'No ID Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + await dbHelper.insertCard(cardWithoutId); + final cards = await dbHelper.getUserCards(); + + expect(cards.length, equals(1)); + expect(cards[0].id, isNotNull); // Should be auto-generated + expect(cards[0].name, equals('No ID Card')); + }); + + test('should handle cards with identical names but different codes', () async { + final card1 = UserCard( + name: 'Duplicate Name', + code: '111111111', + usage: 1, + createdAt: testDate, + symbology: 'CODE128', + ); + + final card2 = UserCard( + name: 'Duplicate Name', + code: '222222222', + usage: 2, + createdAt: testDate, + symbology: 'QR_CODE', + ); + + await dbHelper.insertCard(card1); + await dbHelper.insertCard(card2); + + final cards = await dbHelper.getUserCards(); + expect(cards.length, equals(2)); + expect(cards.where((card) => card.name == 'Duplicate Name').length, equals(2)); + }); + + test('should handle sorting with equal values', () async { + final sameDateCards = [ + UserCard(name: 'Card A', code: '111', usage: 5, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'Card B', code: '222', usage: 5, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'Card C', code: '333', usage: 5, createdAt: testDate, symbology: 'EAN13'), + ]; + + for (final card in sameDateCards) { + await dbHelper.insertCard(card); + } + + final sortedByUsage = await dbHelper.getUserCardsSorted(SortOptions.byUsage); + expect(sortedByUsage.length, equals(3)); + // All have same usage, so order might vary but all should be present + expect(sortedByUsage.map((card) => card.usage).toSet(), equals({5})); + }); + + test('should handle database with mixed symbology types', () async { + final mixedCards = [ + UserCard(name: 'Barcode', code: '111', usage: 1, createdAt: testDate, symbology: 'CODE128'), + UserCard(name: 'QR Code', code: '222', usage: 2, createdAt: testDate, symbology: 'QR_CODE'), + UserCard(name: 'EAN Code', code: '333', usage: 3, createdAt: testDate, symbology: 'EAN13'), + UserCard(name: 'PDF417', code: '444', usage: 4, createdAt: testDate, symbology: 'PDF417'), + UserCard(name: 'Data Matrix', code: '555', usage: 5, createdAt: testDate, symbology: 'DATA_MATRIX'), + ]; + + for (final card in mixedCards) { + await dbHelper.insertCard(card); + } + + final allCards = await dbHelper.getUserCards(); + expect(allCards.length, equals(5)); + + final symbologies = allCards.map((card) => card.symbology).toSet(); + expect(symbologies.length, equals(5)); // All different symbologies + }); + + test('should handle rapid successive database operations', () async { + final card = UserCard( + name: 'Rapid Test Card', + code: '123456789', + usage: 0, + createdAt: testDate, + symbology: 'CODE128', + ); + + // Insert, update, increment, retrieve in rapid succession + await dbHelper.insertCard(card); + final cards = await dbHelper.getUserCards(); + final cardId = cards[0].id!; + + final updatedCard = card.copyWith(id: cardId, name: 'Updated Rapid Card'); + await dbHelper.updateUserCard(updatedCard); + await dbHelper.incrementUsage(cardId); + + final finalCard = await dbHelper.getOneCard(cardId); + expect(finalCard.name, equals('Updated Rapid Card')); + expect(finalCard.usage, equals(1)); + }); + }); +} \ No newline at end of file diff --git a/test/providers/theme_provider_test.dart b/test/providers/theme_provider_test.dart new file mode 100644 index 0000000..e81c583 --- /dev/null +++ b/test/providers/theme_provider_test.dart @@ -0,0 +1,476 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Import the ThemeProvider class +import '../../lib/providers/theme_provider.dart'; + +void main() { + group('ThemeProvider', () { + late ThemeProvider themeProvider; + + setUp(() { + themeProvider = ThemeProvider(); + }); + + group('initialization', () { + test('should initialize with default green seed color', () { + expect(themeProvider.seedColor, equals(Colors.green)); + }); + + test('should initialize with system theme mode', () { + expect(themeProvider.themeMode, equals(ThemeMode.system)); + }); + + test('should be instance of ChangeNotifier', () { + expect(themeProvider, isA()); + }); + }); + + group('seedColor getter', () { + test('should return current seed color', () { + expect(themeProvider.seedColor, equals(Colors.green)); + }); + + test('should return updated seed color after change', () { + themeProvider.setSeedColor(Colors.blue); + expect(themeProvider.seedColor, equals(Colors.blue)); + }); + }); + + group('lightTheme getter', () { + test('should return ThemeData with light brightness', () { + final theme = themeProvider.lightTheme; + + expect(theme, isA()); + expect(theme.colorScheme.brightness, equals(Brightness.light)); + }); + + test('should use current seed color for light theme', () { + themeProvider.setSeedColor(Colors.red); + final theme = themeProvider.lightTheme; + + expect(theme.colorScheme.brightness, equals(Brightness.light)); + // Verify the theme uses the red seed color by checking primary color derivation + expect(theme.colorScheme.primary, isNotNull); + }); + + test('should generate different themes for different seed colors', () { + final greenTheme = themeProvider.lightTheme; + + themeProvider.setSeedColor(Colors.blue); + final blueTheme = themeProvider.lightTheme; + + expect(greenTheme.colorScheme.primary, isNot(equals(blueTheme.colorScheme.primary))); + }); + + test('should always return light brightness regardless of seed color', () { + final colors = [Colors.red, Colors.blue, Colors.purple, Colors.orange]; + + for (final color in colors) { + themeProvider.setSeedColor(color); + final theme = themeProvider.lightTheme; + expect(theme.colorScheme.brightness, equals(Brightness.light)); + } + }); + }); + + group('darkTheme getter', () { + test('should return ThemeData with dark brightness', () { + final theme = themeProvider.darkTheme; + + expect(theme, isA()); + expect(theme.colorScheme.brightness, equals(Brightness.dark)); + }); + + test('should use current seed color for dark theme', () { + themeProvider.setSeedColor(Colors.purple); + final theme = themeProvider.darkTheme; + + expect(theme.colorScheme.brightness, equals(Brightness.dark)); + expect(theme.colorScheme.primary, isNotNull); + }); + + test('should generate different themes for different seed colors', () { + final greenTheme = themeProvider.darkTheme; + + themeProvider.setSeedColor(Colors.yellow); + final yellowTheme = themeProvider.darkTheme; + + expect(greenTheme.colorScheme.primary, isNot(equals(yellowTheme.colorScheme.primary))); + }); + + test('should always return dark brightness regardless of seed color', () { + final colors = [Colors.teal, Colors.pink, Colors.indigo, Colors.amber]; + + for (final color in colors) { + themeProvider.setSeedColor(color); + final theme = themeProvider.darkTheme; + expect(theme.colorScheme.brightness, equals(Brightness.dark)); + } + }); + }); + + group('themeMode getter', () { + test('should return current theme mode', () { + expect(themeProvider.themeMode, equals(ThemeMode.system)); + }); + + test('should return updated theme mode after change', () { + themeProvider.setThemeMode(ThemeMode.dark); + expect(themeProvider.themeMode, equals(ThemeMode.dark)); + }); + }); + + group('setSeedColor method', () { + test('should update seed color', () { + themeProvider.setSeedColor(Colors.red); + expect(themeProvider.seedColor, equals(Colors.red)); + }); + + test('should notify listeners when seed color changes', () { + bool notified = false; + themeProvider.addListener(() { + notified = true; + }); + + themeProvider.setSeedColor(Colors.blue); + expect(notified, isTrue); + }); + + test('should notify listeners even when setting same color', () { + themeProvider.setSeedColor(Colors.orange); + + bool notified = false; + themeProvider.addListener(() { + notified = true; + }); + + themeProvider.setSeedColor(Colors.orange); + expect(notified, isTrue); + }); + + test('should accept all valid Color values', () { + final testColors = [ + Colors.red, + Colors.blue, + Colors.green, + Colors.yellow, + Colors.purple, + Colors.orange, + Colors.pink, + Colors.teal, + Colors.indigo, + Colors.cyan, + Colors.amber, + Colors.lime, + const Color(0xFF123456), // Custom hex color + const Color.fromARGB(255, 100, 150, 200), // Custom ARGB color + const Color.fromRGBO(50, 100, 150, 0.8), // Custom RGBO color + ]; + + for (final color in testColors) { + expect(() => themeProvider.setSeedColor(color), returnsNormally); + expect(themeProvider.seedColor, equals(color)); + } + }); + + test('should update both light and dark themes when seed color changes', () { + final originalLightPrimary = themeProvider.lightTheme.colorScheme.primary; + final originalDarkPrimary = themeProvider.darkTheme.colorScheme.primary; + + themeProvider.setSeedColor(Colors.deepPurple); + + final newLightPrimary = themeProvider.lightTheme.colorScheme.primary; + final newDarkPrimary = themeProvider.darkTheme.colorScheme.primary; + + expect(newLightPrimary, isNot(equals(originalLightPrimary))); + expect(newDarkPrimary, isNot(equals(originalDarkPrimary))); + }); + }); + + group('setThemeMode method', () { + test('should update theme mode', () { + themeProvider.setThemeMode(ThemeMode.light); + expect(themeProvider.themeMode, equals(ThemeMode.light)); + }); + + test('should notify listeners when theme mode changes', () { + bool notified = false; + themeProvider.addListener(() { + notified = true; + }); + + themeProvider.setThemeMode(ThemeMode.dark); + expect(notified, isTrue); + }); + + test('should notify listeners even when setting same mode', () { + themeProvider.setThemeMode(ThemeMode.light); + + bool notified = false; + themeProvider.addListener(() { + notified = true; + }); + + themeProvider.setThemeMode(ThemeMode.light); + expect(notified, isTrue); + }); + + test('should accept all ThemeMode values', () { + final themeModes = [ + ThemeMode.system, + ThemeMode.light, + ThemeMode.dark, + ]; + + for (final mode in themeModes) { + expect(() => themeProvider.setThemeMode(mode), returnsNormally); + expect(themeProvider.themeMode, equals(mode)); + } + }); + }); + + group('ChangeNotifier behavior', () { + test('should support multiple listeners', () { + int listener1Called = 0; + int listener2Called = 0; + int listener3Called = 0; + + themeProvider.addListener(() => listener1Called++); + themeProvider.addListener(() => listener2Called++); + themeProvider.addListener(() => listener3Called++); + + themeProvider.setSeedColor(Colors.red); + + expect(listener1Called, equals(1)); + expect(listener2Called, equals(1)); + expect(listener3Called, equals(1)); + }); + + test('should stop notifying removed listeners', () { + int callCount = 0; + void listener() => callCount++; + + themeProvider.addListener(listener); + themeProvider.setSeedColor(Colors.blue); + expect(callCount, equals(1)); + + themeProvider.removeListener(listener); + themeProvider.setSeedColor(Colors.red); + expect(callCount, equals(1)); // Should not increment + }); + + test('should handle listener removal during notification', () { + int callCount = 0; + late VoidCallback listener; + + listener = () { + callCount++; + themeProvider.removeListener(listener); + }; + + themeProvider.addListener(listener); + expect(() => themeProvider.setSeedColor(Colors.purple), returnsNormally); + expect(callCount, equals(1)); + }); + }); + + group('state consistency', () { + test('should maintain consistent state after multiple operations', () { + themeProvider.setSeedColor(Colors.indigo); + themeProvider.setThemeMode(ThemeMode.dark); + + expect(themeProvider.seedColor, equals(Colors.indigo)); + expect(themeProvider.themeMode, equals(ThemeMode.dark)); + expect(themeProvider.lightTheme.colorScheme.brightness, equals(Brightness.light)); + expect(themeProvider.darkTheme.colorScheme.brightness, equals(Brightness.dark)); + }); + + test('should preserve theme mode when seed color changes', () { + themeProvider.setThemeMode(ThemeMode.light); + themeProvider.setSeedColor(Colors.teal); + + expect(themeProvider.themeMode, equals(ThemeMode.light)); + }); + + test('should preserve seed color when theme mode changes', () { + themeProvider.setSeedColor(Colors.deepOrange); + themeProvider.setThemeMode(ThemeMode.dark); + + expect(themeProvider.seedColor, equals(Colors.deepOrange)); + }); + }); + + group('theme generation consistency', () { + test('should generate same theme for same seed color', () { + themeProvider.setSeedColor(Colors.cyan); + final theme1 = themeProvider.lightTheme; + final theme2 = themeProvider.lightTheme; + + expect(theme1.colorScheme.primary, equals(theme2.colorScheme.primary)); + expect(theme1.colorScheme.secondary, equals(theme2.colorScheme.secondary)); + }); + + test('should generate different light and dark themes for same seed', () { + themeProvider.setSeedColor(Colors.lime); + final lightTheme = themeProvider.lightTheme; + final darkTheme = themeProvider.darkTheme; + + expect(lightTheme.colorScheme.brightness, equals(Brightness.light)); + expect(darkTheme.colorScheme.brightness, equals(Brightness.dark)); + expect(lightTheme.colorScheme.surface, isNot(equals(darkTheme.colorScheme.surface))); + expect(lightTheme.colorScheme.onSurface, isNot(equals(darkTheme.colorScheme.onSurface))); + }); + }); + + group('edge cases', () { + test('should handle rapid consecutive calls', () { + int notificationCount = 0; + themeProvider.addListener(() => notificationCount++); + + for (int i = 0; i < 100; i++) { + themeProvider.setSeedColor(Color(0xFF000000 + i)); + } + + expect(notificationCount, equals(100)); + }); + + test('should handle alternating between two colors', () { + final colors = [Colors.black, Colors.white]; + int notificationCount = 0; + themeProvider.addListener(() => notificationCount++); + + for (int i = 0; i < 10; i++) { + themeProvider.setSeedColor(colors[i % 2]); + } + + expect(notificationCount, equals(10)); + expect(themeProvider.seedColor, equals(Colors.black)); + }); + + test('should handle alternating between theme modes', () { + final modes = [ThemeMode.light, ThemeMode.dark, ThemeMode.system]; + int notificationCount = 0; + themeProvider.addListener(() => notificationCount++); + + for (int i = 0; i < 9; i++) { + themeProvider.setThemeMode(modes[i % 3]); + } + + expect(notificationCount, equals(9)); + expect(themeProvider.themeMode, equals(ThemeMode.system)); + }); + }); + + group('ColorScheme properties verification', () { + test('should generate valid ColorScheme for light theme', () { + themeProvider.setSeedColor(Colors.blue); + final theme = themeProvider.lightTheme; + final colorScheme = theme.colorScheme; + + expect(colorScheme.brightness, equals(Brightness.light)); + expect(colorScheme.primary, isNotNull); + expect(colorScheme.onPrimary, isNotNull); + expect(colorScheme.secondary, isNotNull); + expect(colorScheme.onSecondary, isNotNull); + expect(colorScheme.surface, isNotNull); + expect(colorScheme.onSurface, isNotNull); + expect(colorScheme.background, isNotNull); + expect(colorScheme.onBackground, isNotNull); + }); + + test('should generate valid ColorScheme for dark theme', () { + themeProvider.setSeedColor(Colors.red); + final theme = themeProvider.darkTheme; + final colorScheme = theme.colorScheme; + + expect(colorScheme.brightness, equals(Brightness.dark)); + expect(colorScheme.primary, isNotNull); + expect(colorScheme.onPrimary, isNotNull); + expect(colorScheme.secondary, isNotNull); + expect(colorScheme.onSecondary, isNotNull); + expect(colorScheme.surface, isNotNull); + expect(colorScheme.onSurface, isNotNull); + expect(colorScheme.background, isNotNull); + expect(colorScheme.onBackground, isNotNull); + }); + }); + + group('extreme color values', () { + test('should handle pure black seed color', () { + expect(() => themeProvider.setSeedColor(Colors.black), returnsNormally); + expect(themeProvider.lightTheme.colorScheme.brightness, equals(Brightness.light)); + expect(themeProvider.darkTheme.colorScheme.brightness, equals(Brightness.dark)); + }); + + test('should handle pure white seed color', () { + expect(() => themeProvider.setSeedColor(Colors.white), returnsNormally); + expect(themeProvider.lightTheme.colorScheme.brightness, equals(Brightness.light)); + expect(themeProvider.darkTheme.colorScheme.brightness, equals(Brightness.dark)); + }); + + test('should handle transparent seed color', () { + expect(() => themeProvider.setSeedColor(Colors.transparent), returnsNormally); + expect(themeProvider.lightTheme.colorScheme.brightness, equals(Brightness.light)); + expect(themeProvider.darkTheme.colorScheme.brightness, equals(Brightness.dark)); + }); + + test('should handle very bright colors', () { + final brightColors = [ + const Color(0xFFFFFFFF), + const Color(0xFFFF0000), + const Color(0xFF00FF00), + const Color(0xFF0000FF), + ]; + + for (final color in brightColors) { + expect(() => themeProvider.setSeedColor(color), returnsNormally); + expect(themeProvider.seedColor, equals(color)); + } + }); + + test('should handle very dark colors', () { + final darkColors = [ + const Color(0xFF000000), + const Color(0xFF330000), + const Color(0xFF003300), + const Color(0xFF000033), + ]; + + for (final color in darkColors) { + expect(() => themeProvider.setSeedColor(color), returnsNormally); + expect(themeProvider.seedColor, equals(color)); + } + }); + }); + + group('memory and performance', () { + test('should not leak listeners on repeated operations', () { + // Add and remove listeners multiple times + for (int i = 0; i < 10; i++) { + void listener() {} + themeProvider.addListener(listener); + themeProvider.removeListener(listener); + } + + // Should still work normally + bool notified = false; + themeProvider.addListener(() => notified = true); + themeProvider.setSeedColor(Colors.pink); + expect(notified, isTrue); + }); + + test('should handle many rapid color changes efficiently', () { + final stopwatch = Stopwatch()..start(); + + for (int i = 0; i < 1000; i++) { + themeProvider.setSeedColor(Color(0xFF000000 + (i % 0xFFFFFF))); + } + + stopwatch.stop(); + // Verify operations complete in reasonable time (adjust as needed) + expect(stopwatch.elapsedMilliseconds, lessThan(5000)); + }); + }); + }); +} \ No newline at end of file diff --git a/test/settings_test.dart b/test/settings_test.dart new file mode 100644 index 0000000..52ae712 --- /dev/null +++ b/test/settings_test.dart @@ -0,0 +1,695 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:stashcard/Views/settings.dart'; +import 'package:stashcard/providers/theme_provider.dart'; + +import 'settings_test.mocks.dart'; + +// Generate mocks for testing +@GenerateMocks([ThemeProvider]) +void main() { + group('AppThemeMode', () { + test('should have correct display names', () { + expect(AppThemeMode.system.displayName, equals('System')); + expect(AppThemeMode.light.displayName, equals('Light')); + expect(AppThemeMode.dark.displayName, equals('Dark')); + }); + + test('toFlutterThemeMode should convert correctly', () { + expect(AppThemeMode.system.toFlutterThemeMode(), equals(ThemeMode.system)); + expect(AppThemeMode.light.toFlutterThemeMode(), equals(ThemeMode.light)); + expect(AppThemeMode.dark.toFlutterThemeMode(), equals(ThemeMode.dark)); + }); + + test('fromFlutterThemeMode should convert correctly', () { + expect(AppThemeMode.fromFlutterThemeMode(ThemeMode.system), equals(AppThemeMode.system)); + expect(AppThemeMode.fromFlutterThemeMode(ThemeMode.light), equals(AppThemeMode.light)); + expect(AppThemeMode.fromFlutterThemeMode(ThemeMode.dark), equals(AppThemeMode.dark)); + }); + + test('should handle all enum values in conversion methods', () { + // Test that conversion works for all values + for (final mode in AppThemeMode.values) { + final flutterMode = mode.toFlutterThemeMode(); + final backToAppMode = AppThemeMode.fromFlutterThemeMode(flutterMode); + expect(backToAppMode, equals(mode)); + } + }); + + test('enum should have exactly 3 values', () { + expect(AppThemeMode.values.length, equals(3)); + }); + + test('enum values should be unique', () { + final displayNames = AppThemeMode.values.map((e) => e.displayName).toList(); + final uniqueNames = displayNames.toSet(); + expect(displayNames.length, equals(uniqueNames.length)); + }); + }); + + group('SettingsPage Widget Tests', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should render all settings options correctly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Verify app bar + expect(find.text('Settings'), findsOneWidget); + expect(find.byType(AppBar), findsOneWidget); + + // Verify all list tiles with their exact text + expect(find.text('App color scheme'), findsOneWidget); + expect(find.text('App theme'), findsOneWidget); + expect(find.text('App lock'), findsOneWidget); + expect(find.text('Source code'), findsOneWidget); + expect(find.text('Copyright © 2025 LahevOdVika'), findsOneWidget); + + // Verify corresponding icons + expect(find.byIcon(Icons.color_lens), findsOneWidget); + expect(find.byIcon(Icons.brightness_4), findsOneWidget); + expect(find.byIcon(Icons.lock), findsOneWidget); + expect(find.byIcon(Icons.code), findsOneWidget); + + // Verify correct number of dividers + expect(find.byType(Divider), findsNWidgets(4)); + + // Verify ListView for scrollability + expect(find.byType(ListView), findsOneWidget); + }); + + testWidgets('should open color picker dialog when color scheme is tapped', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Tap on color scheme option + await tester.tap(find.text('App color scheme')); + await tester.pumpAndSettle(); + + // Verify dialog appears with correct components + expect(find.text('Pick a color'), findsOneWidget); + expect(find.byType(AlertDialog), findsOneWidget); + expect(find.byType(BlockPicker), findsOneWidget); + }); + + testWidgets('should call setSeedColor and close dialog when color is selected', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Open color picker dialog + await tester.tap(find.text('App color scheme')); + await tester.pumpAndSettle(); + + // Find the BlockPicker widget + final blockPicker = find.byType(BlockPicker); + expect(blockPicker, findsOneWidget); + + // Simulate color selection by directly calling the onColorChanged callback + final widget = tester.widget(blockPicker); + widget.onColorChanged(Colors.red); + + await tester.pumpAndSettle(); + + // Verify setSeedColor was called with the new color + verify(mockThemeProvider.setSeedColor(Colors.red)).called(1); + + // Verify dialog is closed (no longer visible) + expect(find.text('Pick a color'), findsNothing); + }); + + testWidgets('should show dropdown menu with correct theme options', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Find dropdown menu + expect(find.byType(DropdownMenu), findsOneWidget); + + // Verify initial selection matches theme provider + verify(mockThemeProvider.themeMode).called(greaterThan(0)); + }); + + testWidgets('should call setThemeMode when theme is selected from dropdown', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final dropdown = tester.widget>( + find.byType(DropdownMenu) + ); + + // Simulate theme selection to Dark mode + dropdown.onSelected?.call(AppThemeMode.dark); + await tester.pumpAndSettle(); + + // Verify setThemeMode was called with correct theme + verify(mockThemeProvider.setThemeMode(ThemeMode.dark)).called(1); + }); + + testWidgets('should handle null selection in theme dropdown gracefully', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final dropdown = tester.widget>( + find.byType(DropdownMenu) + ); + + // Simulate null selection + dropdown.onSelected?.call(null); + await tester.pumpAndSettle(); + + // Verify setThemeMode was not called with null + verifyNever(mockThemeProvider.setThemeMode(any)); + }); + + testWidgets('should update controller text when valid theme is selected', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final dropdown = tester.widget>( + find.byType(DropdownMenu) + ); + + // Simulate theme selection to Light mode + dropdown.onSelected?.call(AppThemeMode.light); + await tester.pumpAndSettle(); + + // Verify both setThemeMode was called and controller would be updated + verify(mockThemeProvider.setThemeMode(ThemeMode.light)).called(1); + }); + + testWidgets('should render app lock option without tap functionality', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Find app lock tile + final lockTile = find.ancestor( + of: find.text('App lock'), + matching: find.byType(ListTile), + ); + expect(lockTile, findsOneWidget); + + // Verify it has no onTap functionality (future feature) + final lockListTile = tester.widget(lockTile); + expect(lockListTile.onTap, isNull); + }); + + testWidgets('should render copyright information correctly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + expect(find.text('Copyright © 2025 LahevOdVika'), findsOneWidget); + + // Verify it's in a ListTile trailing position + final copyrightTile = find.ancestor( + of: find.text('Copyright © 2025 LahevOdVika'), + matching: find.byType(ListTile), + ); + expect(copyrightTile, findsOneWidget); + }); + + testWidgets('should have correct GitHub URL in state', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Access the private state to verify GitHub URL + final settingsPageState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + expect(settingsPageState.githubUrl, equals('https://github.com/LahevOdVika/Stashcard')); + }); + + testWidgets('should render source code button correctly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Find source code button + final sourceCodeButton = find.ancestor( + of: find.text('Source code'), + matching: find.byType(TextButton), + ); + expect(sourceCodeButton, findsOneWidget); + + // Verify button has icon and label + expect(find.byIcon(Icons.code), findsOneWidget); + expect(find.text('Source code'), findsOneWidget); + + // Test that button is tappable (we can't test URL launching without mocking url_launcher) + await tester.tap(sourceCodeButton); + await tester.pump(); + + // No exceptions should be thrown during tap + }); + }); + + group('Edge Cases and Error Handling', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should handle different initial theme provider values', (WidgetTester tester) async { + // Test with Dark theme initially + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.dark); + when(mockThemeProvider.seedColor).thenReturn(Colors.red); + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Should still render correctly + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + + // Verify theme provider methods were called + verify(mockThemeProvider.themeMode).called(greaterThan(0)); + verify(mockThemeProvider.seedColor).called(greaterThan(0)); + }); + + testWidgets('should handle theme provider with light mode', (WidgetTester tester) async { + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.light); + when(mockThemeProvider.seedColor).thenReturn(Colors.green); + + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + expect(find.byType(SettingsPage), findsOneWidget); + + // Verify dropdown shows correct initial selection + final dropdown = tester.widget>( + find.byType(DropdownMenu) + ); + expect(dropdown.initialSelection, equals(AppThemeMode.light)); + }); + + testWidgets('should maintain controller state across rebuilds', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Get initial controller + final initialState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + final initialController = initialState._themeModeController; + + // Trigger rebuild + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Controller should be the same instance + final rebuiltState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + expect(rebuiltState._themeModeController, equals(initialController)); + }); + + testWidgets('should handle rapid color changes', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Open color picker + await tester.tap(find.text('App color scheme')); + await tester.pumpAndSettle(); + + final blockPicker = tester.widget(find.byType(BlockPicker)); + + // Simulate rapid color changes + blockPicker.onColorChanged(Colors.red); + blockPicker.onColorChanged(Colors.green); + blockPicker.onColorChanged(Colors.blue); + + await tester.pumpAndSettle(); + + // Should handle all calls + verify(mockThemeProvider.setSeedColor(Colors.red)).called(1); + verify(mockThemeProvider.setSeedColor(Colors.green)).called(1); + verify(mockThemeProvider.setSeedColor(Colors.blue)).called(1); + }); + }); + + group('Accessibility and UI Tests', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should have proper semantic structure', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Verify semantic structure + expect(find.byType(ListTile), findsNWidgets(5)); + + // All ListTiles should have titles for accessibility + final listTiles = tester.widgetList(find.byType(ListTile)); + for (final tile in listTiles) { + expect(tile.title != null || tile.trailing != null, isTrue); + } + }); + + testWidgets('should have adequate tap target sizes', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Check tap target sizes meet minimum requirements (48dp) + final listTileSize = tester.getSize(find.byType(ListTile).first); + expect(listTileSize.height, greaterThanOrEqualTo(48.0)); + + final buttonSize = tester.getSize(find.byType(TextButton)); + expect(buttonSize.height, greaterThanOrEqualTo(48.0)); + }); + + testWidgets('should support scrolling when content overflows', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Verify ListView enables scrolling + expect(find.byType(ListView), findsOneWidget); + + // Test scrolling capability + await tester.drag(find.byType(ListView), const Offset(0, -100)); + await tester.pumpAndSettle(); + + // Should not throw any exceptions + }); + + testWidgets('should maintain visual hierarchy with dividers', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Verify dividers separate sections logically + final dividers = find.byType(Divider); + expect(dividers, findsNWidgets(4)); + + // Check divider positioning + final listItems = find.byType(ListTile); + expect(listItems, findsNWidgets(5)); + }); + + testWidgets('should handle different screen orientations', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Test portrait mode (default) + expect(find.byType(SettingsPage), findsOneWidget); + + // Simulate orientation change by changing screen size + tester.binding.window.physicalSizeTestValue = const Size(2400, 1080); // Landscape + tester.binding.window.devicePixelRatioTestValue = 3.0; + await tester.pumpAndSettle(); + + // Should still render correctly + expect(find.byType(SettingsPage), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); + + // Reset to portrait for other tests + tester.binding.window.physicalSizeTestValue = const Size(1080, 2400); + addTearDown(() => tester.binding.window.clearPhysicalSizeTestValue()); + }); + }); + + group('Integration with ThemeProvider', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should react to theme provider changes', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Verify initial state + verify(mockThemeProvider.themeMode).called(greaterThan(0)); + verify(mockThemeProvider.seedColor).called(greaterThan(0)); + + // Test theme mode change + final dropdown = tester.widget>( + find.byType(DropdownMenu) + ); + dropdown.onSelected?.call(AppThemeMode.dark); + + verify(mockThemeProvider.setThemeMode(ThemeMode.dark)).called(1); + }); + + testWidgets('should use Provider.of with listen: false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // The widget should access theme provider in build method + // This verifies the Provider.of(context, listen: false) call + verify(mockThemeProvider.themeMode).called(greaterThan(0)); + verify(mockThemeProvider.seedColor).called(greaterThan(0)); + }); + }); + + group('URL Launching Tests', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should handle URL launch exceptions gracefully', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + // Find and tap source code button + final sourceCodeButton = find.ancestor( + of: find.text('Source code'), + matching: find.byType(TextButton), + ); + expect(sourceCodeButton, findsOneWidget); + + // Tap the button - this would normally try to launch URL + // We expect it to handle any exceptions gracefully + expect(() => tester.tap(sourceCodeButton), returnsNormally); + await tester.pump(); + }); + + testWidgets('should have valid GitHub URL format', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final settingsPageState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + final url = settingsPageState.githubUrl; + + // Verify URL format + expect(url, startsWith('https://')); + expect(url, contains('github.com')); + expect(Uri.tryParse(url), isNotNull); + }); + }); + + group('Widget State Management', () { + late MockThemeProvider mockThemeProvider; + + setUp(() { + mockThemeProvider = MockThemeProvider(); + when(mockThemeProvider.seedColor).thenReturn(Colors.blue); + when(mockThemeProvider.themeMode).thenReturn(ThemeMode.system); + }); + + testWidgets('should initialize with correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final settingsPageState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + + // Verify initialization + expect(settingsPageState.githubUrl, isNotEmpty); + expect(settingsPageState._themeModeController, isNotNull); + expect(settingsPageState.selectedTheme, isNull); // Initially null + }); + + testWidgets('should handle controller disposal properly', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: mockThemeProvider, + child: const SettingsPage(), + ), + ), + ); + + final settingsPageState = tester.state<_SettingsPageState>(find.byType(SettingsPage)); + final controller = settingsPageState._themeModeController; + + // Verify controller is not disposed initially + expect(() => controller.text, returnsNormally); + + // Remove widget to trigger dispose + await tester.pumpWidget(const MaterialApp(home: Scaffold())); + + // Controller should be disposed (this would throw if accessed) + // We can't easily test this without accessing private state + }); + }); +} \ No newline at end of file diff --git a/test/settings_test.mocks.dart b/test/settings_test.mocks.dart new file mode 100644 index 0000000..7cb6bb5 --- /dev/null +++ b/test/settings_test.mocks.dart @@ -0,0 +1,133 @@ +// Mocks generated by Mockito 5.4.4 from annotations +// in stashcard/test/settings_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:flutter/foundation.dart' as _i4; +import 'package:flutter/material.dart' as _i5; +import 'package:mockito/mockito.dart' as _i1; +import 'package:stashcard/providers/theme_provider.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [ThemeProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockThemeProvider extends _i1.Mock implements _i2.ThemeProvider { + MockThemeProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Color get seedColor => (super.noSuchMethod( + Invocation.getter(#seedColor), + returnValue: const _i5.Color(0xFF4CAF50), + ) as _i5.Color); + + @override + _i5.ThemeData get lightTheme => (super.noSuchMethod( + Invocation.getter(#lightTheme), + returnValue: _FakeThemeData_0( + this, + Invocation.getter(#lightTheme), + ), + ) as _i5.ThemeData); + + @override + _i5.ThemeData get darkTheme => (super.noSuchMethod( + Invocation.getter(#darkTheme), + returnValue: _FakeThemeData_0( + this, + Invocation.getter(#darkTheme), + ), + ) as _i5.ThemeData); + + @override + _i5.ThemeMode get themeMode => (super.noSuchMethod( + Invocation.getter(#themeMode), + returnValue: _i5.ThemeMode.system, + ) as _i5.ThemeMode); + + @override + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); + + @override + void setSeedColor(_i5.Color? color) => super.noSuchMethod( + Invocation.method( + #setSeedColor, + [color], + ), + returnValueForMissingStub: null, + ); + + @override + void setThemeMode(_i5.ThemeMode? mode) => super.noSuchMethod( + Invocation.method( + #setThemeMode, + [mode], + ), + returnValueForMissingStub: null, + ); + + @override + void addListener(_i4.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #addListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void removeListener(_i4.VoidCallback? listener) => super.noSuchMethod( + Invocation.method( + #removeListener, + [listener], + ), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); + + @override + void notifyListeners() => super.noSuchMethod( + Invocation.method( + #notifyListeners, + [], + ), + returnValueForMissingStub: null, + ); +} + +class _FakeThemeData_0 extends _i1.SmartFake implements _i5.ThemeData { + _FakeThemeData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} \ No newline at end of file