cyberhybridhub/test/widgets/spelling_list_screen_test.dart
2026-06-07 03:10:21 +00:00

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);
});
}