191 lines
5.7 KiB
Dart
191 lines
5.7 KiB
Dart
import 'package:cyberhybridhub/data/remote/spelling_words_remote_store.dart';
|
|
import 'package:cyberhybridhub/screens/spelling_list_screen.dart';
|
|
import 'package:cyberhybridhub/theme/app_theme.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
class MockSpellingWordsRemoteStore extends SpellingWordsRemoteStore {
|
|
MockSpellingWordsRemoteStore({
|
|
this.words = const <String>[],
|
|
this.fetchWordsResult,
|
|
this.deleteWordResult,
|
|
this.deleteWordError,
|
|
});
|
|
|
|
List<String> words;
|
|
List<String>? fetchWordsResult;
|
|
List<String>? deleteWordResult;
|
|
Object? deleteWordError;
|
|
|
|
final List<String> deletedWords = <String>[];
|
|
int fetchCount = 0;
|
|
int deleteCount = 0;
|
|
|
|
@override
|
|
Future<List<String>> fetchWords() async {
|
|
fetchCount++;
|
|
if (fetchWordsResult != null) {
|
|
return fetchWordsResult!;
|
|
}
|
|
return words;
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> deleteWord(String word) async {
|
|
deleteCount++;
|
|
deletedWords.add(word);
|
|
if (deleteWordError != null) {
|
|
throw deleteWordError!;
|
|
}
|
|
if (deleteWordResult != null) {
|
|
return deleteWordResult!;
|
|
}
|
|
final List<String> newWords = List<String>.from(words)..remove(word);
|
|
return newWords;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('lists words loaded from remote store', (WidgetTester tester) async {
|
|
final mockStore = MockSpellingWordsRemoteStore(
|
|
words: ['apple', 'banana', 'cherry'],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: SpellingListScreen(api: mockStore),
|
|
),
|
|
);
|
|
|
|
// Shows loading first
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify loading is gone and words are displayed
|
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
|
expect(find.text('apple'), findsOneWidget);
|
|
expect(find.text('banana'), findsOneWidget);
|
|
expect(find.text('cherry'), findsOneWidget);
|
|
expect(find.byKey(const Key('spelling-word-delete-apple')), findsOneWidget);
|
|
expect(find.byKey(const Key('spelling-word-delete-banana')), findsOneWidget);
|
|
expect(find.byKey(const Key('spelling-word-delete-cherry')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('delete button cancel does not call delete API and leaves list intact',
|
|
(WidgetTester tester) async {
|
|
final mockStore = MockSpellingWordsRemoteStore(
|
|
words: ['apple', 'banana'],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: SpellingListScreen(api: mockStore),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap delete button for apple
|
|
await tester.tap(find.byKey(const Key('spelling-word-delete-apple')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Verify confirmation dialog is visible
|
|
expect(find.byKey(const Key('spelling-word-delete-confirm-dialog')), findsOneWidget);
|
|
expect(find.text('Delete spelling word?'), findsOneWidget);
|
|
expect(find.textContaining('This deletes "apple"'), findsOneWidget);
|
|
|
|
// Tap Cancel
|
|
await tester.tap(find.text('Cancel'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Dialog should be gone
|
|
expect(find.byKey(const Key('spelling-word-delete-confirm-dialog')), findsNothing);
|
|
|
|
// Store is not called
|
|
expect(mockStore.deleteCount, 0);
|
|
expect(mockStore.deletedWords, isEmpty);
|
|
|
|
// Words still exist
|
|
expect(find.text('apple'), findsOneWidget);
|
|
expect(find.text('banana'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('delete button confirm calls delete API, updates UI list, and shows snackbar',
|
|
(WidgetTester tester) async {
|
|
final mockStore = MockSpellingWordsRemoteStore(
|
|
words: ['apple', 'banana'],
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: SpellingListScreen(api: mockStore),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap delete button for apple
|
|
await tester.tap(find.byKey(const Key('spelling-word-delete-apple')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap Delete inside dialog
|
|
await tester.tap(find.byKey(const Key('spelling-word-delete-confirm-button')));
|
|
await tester.pump(); // Start async action
|
|
|
|
// Verify mockStore is called
|
|
expect(mockStore.deleteCount, 1);
|
|
expect(mockStore.deletedWords, ['apple']);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
// Dialog should be gone
|
|
expect(find.byKey(const Key('spelling-word-delete-confirm-dialog')), findsNothing);
|
|
|
|
// List is updated
|
|
expect(find.text('apple'), findsNothing);
|
|
expect(find.text('banana'), findsOneWidget);
|
|
|
|
// Snackbar is shown
|
|
expect(find.text('Deleted "apple"'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('delete failure shows error snackbar and retains word in list',
|
|
(WidgetTester tester) async {
|
|
final mockStore = MockSpellingWordsRemoteStore(
|
|
words: ['apple', 'banana'],
|
|
deleteWordError: Exception('Failed to delete'),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: SpellingListScreen(api: mockStore),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap delete button for apple
|
|
await tester.tap(find.byKey(const Key('spelling-word-delete-apple')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tap Delete inside dialog
|
|
await tester.tap(find.byKey(const Key('spelling-word-delete-confirm-button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Dialog is gone
|
|
expect(find.byKey(const Key('spelling-word-delete-confirm-dialog')), findsNothing);
|
|
|
|
// Store was called
|
|
expect(mockStore.deleteCount, 1);
|
|
|
|
// List is NOT updated, apple is still there
|
|
expect(find.text('apple'), findsOneWidget);
|
|
expect(find.text('banana'), findsOneWidget);
|
|
|
|
// Error snackbar is shown
|
|
expect(find.text('Could not delete word.'), findsOneWidget);
|
|
});
|
|
}
|