refactor: create separate BLoCs for each screen with comprehensive tests

- Created 8 separate BLoCs (Home, Library, BookDetails, AddBook, Scanner, Categories, Wishlist, Settings)
- Each BLoC has its own event, state, and bloc files
- Added 70 comprehensive tests covering all BLoC functionality
- All tests passing (70/70)
- Fixed linting issues and updated deprecated APIs
- Improved code organization and maintainability
This commit is contained in:
Yuriy Panov
2026-02-04 14:40:00 +06:00
parent 3004f712f3
commit 310463e89a
177 changed files with 9718 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/add_book/add_book_bloc.dart';
import 'package:bookshelf_flutter/bloc/add_book/add_book_event.dart';
import 'package:bookshelf_flutter/bloc/add_book/add_book_state.dart';
import 'package:bookshelf_flutter/models/models.dart';
void main() {
group('AddBookBloc', () {
late AddBookBloc addBookBloc;
setUp(() {
addBookBloc = AddBookBloc();
});
tearDown(() {
addBookBloc.close();
});
test('initial state is AddBookState.initial()', () {
expect(addBookBloc.state, isA<AddBookState>());
expect(addBookBloc.state.isLoading, false);
});
group('InitializeForm', () {
test('initializes form with prefilled data', () async {
final prefilledData = {
'title': 'Test Book',
'author': 'Test Author',
'genre': 'Test Genre',
'annotation': 'Test annotation',
'coverUrl': 'https://example.com/cover.jpg',
'pages': 200,
'language': 'English',
'publishedYear': 2020,
'rating': 4.5,
'status': BookStatus.reading,
'progress': 50,
'isFavorite': true,
};
addBookBloc.add(InitializeForm(prefilledData));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.title, 'Test Book');
expect(addBookBloc.state.author, 'Test Author');
expect(addBookBloc.state.genre, 'Test Genre');
expect(addBookBloc.state.isFavorite, true);
expect(addBookBloc.state.pages, 200);
});
test('initializes form with empty data when prefilledData is null', () async {
addBookBloc.add(const InitializeForm(null));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.title, '');
expect(addBookBloc.state.author, '');
expect(addBookBloc.state.isFavorite, false);
});
});
group('UpdateFormField', () {
test('updates title field', () async {
addBookBloc.add(const UpdateFormField('title', 'New Title'));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.title, 'New Title');
});
test('updates author field', () async {
addBookBloc.add(const UpdateFormField('author', 'New Author'));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.author, 'New Author');
});
test('updates genre field', () async {
addBookBloc.add(const UpdateFormField('genre', 'New Genre'));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.genre, 'New Genre');
});
test('updates pages field', () async {
addBookBloc.add(const UpdateFormField('pages', 300));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.pages, 300);
});
test('updates rating field', () async {
addBookBloc.add(const UpdateFormField('rating', 4.8));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.rating, 4.8);
});
test('updates status field', () async {
addBookBloc.add(const UpdateFormField('status', BookStatus.done));
await Future.delayed(Duration.zero);
expect(addBookBloc.state.status, BookStatus.done);
});
});
group('ToggleFavorite', () {
test('toggles isFavorite from false to true', () async {
expect(addBookBloc.state.isFavorite, false);
addBookBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.isFavorite, true);
});
test('toggles isFavorite from true to false', () async {
addBookBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.isFavorite, true);
addBookBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.isFavorite, false);
});
});
group('ClearForm', () {
test('clears all form fields', () async {
addBookBloc.add(const UpdateFormField('title', 'Test'));
addBookBloc.add(const UpdateFormField('author', 'Test'));
addBookBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.title, 'Test');
addBookBloc.add(const ClearForm());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.title, '');
expect(addBookBloc.state.author, '');
expect(addBookBloc.state.isFavorite, false);
});
});
group('SubmitBook', () {
test('creates book with form data and sets isSubmitted to true', () async {
addBookBloc.add(const UpdateFormField('title', 'New Book'));
addBookBloc.add(const UpdateFormField('author', 'New Author'));
addBookBloc.add(const UpdateFormField('genre', 'New Genre'));
addBookBloc.add(const UpdateFormField('annotation', 'New annotation'));
addBookBloc.add(const UpdateFormField('pages', 200));
addBookBloc.add(const UpdateFormField('language', 'English'));
addBookBloc.add(const UpdateFormField('publishedYear', 2023));
addBookBloc.add(const UpdateFormField('rating', 4.5));
addBookBloc.add(const UpdateFormField('status', BookStatus.wantToRead));
addBookBloc.add(const UpdateFormField('progress', 0));
await Future.delayed(Duration.zero);
addBookBloc.add(const SubmitBook());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.isSubmitted, true);
expect(addBookBloc.state.submittedBook?.title, 'New Book');
expect(addBookBloc.state.submittedBook?.author, 'New Author');
});
test('uses default values for empty fields', () async {
addBookBloc.add(const SubmitBook());
await Future.delayed(Duration.zero);
expect(addBookBloc.state.submittedBook?.title, 'Unknown');
expect(addBookBloc.state.submittedBook?.author, 'Unknown');
expect(addBookBloc.state.submittedBook?.genre, 'Unknown');
expect(addBookBloc.state.submittedBook?.coverUrl?.isNotEmpty, true);
});
});
});
}

View File

@@ -0,0 +1,138 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/book_details/book_details_bloc.dart';
import 'package:bookshelf_flutter/bloc/book_details/book_details_event.dart';
import 'package:bookshelf_flutter/bloc/book_details/book_details_state.dart';
import 'package:bookshelf_flutter/models/models.dart';
void main() {
group('BookDetailsBloc', () {
late BookDetailsBloc bookDetailsBloc;
late Book testBook;
setUp(() {
bookDetailsBloc = BookDetailsBloc();
testBook = createBook(
id: '1',
title: 'Test Book',
author: 'Test Author',
genre: 'Test Genre',
annotation: 'Test annotation',
coverUrl: 'https://example.com/cover.jpg',
pages: 200,
language: 'English',
publishedYear: 2020,
rating: 4.5,
status: BookStatus.reading,
progress: 50,
isFavorite: false,
);
});
tearDown(() {
bookDetailsBloc.close();
});
test('initial state is BookDetailsState.initial()', () {
expect(bookDetailsBloc.state, isA<BookDetailsState>());
expect(bookDetailsBloc.state.isLoading, true);
});
group('LoadBookDetails', () {
test('loads book details into state', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.id, testBook.id);
expect(bookDetailsBloc.state.book?.title, testBook.title);
expect(bookDetailsBloc.state.isLoading, false);
});
});
group('ToggleFavorite', () {
test('toggles isFavorite from false to true', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.isFavorite, true);
});
test('toggles isFavorite from true to false', () async {
final favoriteBook = testBook.copyWith(isFavorite: true);
bookDetailsBloc.add(LoadBookDetails(favoriteBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.isFavorite, false);
});
test('does nothing when no book is loaded', () async {
final initialFavorite = bookDetailsBloc.state.book?.isFavorite;
bookDetailsBloc.add(const ToggleFavorite());
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.isFavorite, initialFavorite);
});
});
group('UpdateProgress', () {
test('updates progress and keeps reading status', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const UpdateProgress(75));
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.progress, 75);
expect(bookDetailsBloc.state.book?.status, BookStatus.reading);
});
test('updates status to done when progress is 100', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const UpdateProgress(100));
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.progress, 100);
expect(bookDetailsBloc.state.book?.status, BookStatus.done);
});
test('does nothing when no book is loaded', () async {
final initialProgress = bookDetailsBloc.state.book?.progress;
bookDetailsBloc.add(const UpdateProgress(50));
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.progress, initialProgress);
});
});
group('UpdateStatus', () {
test('updates book status', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const UpdateStatus(BookStatus.done));
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.book?.status, BookStatus.done);
});
});
group('DeleteBook', () {
test('sets isDeleted to true', () async {
bookDetailsBloc.add(LoadBookDetails(testBook));
await Future.delayed(Duration.zero);
bookDetailsBloc.add(const DeleteBook());
await Future.delayed(Duration.zero);
expect(bookDetailsBloc.state.isDeleted, true);
});
});
});
}

View File

@@ -0,0 +1,81 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/categories/categories_bloc.dart';
import 'package:bookshelf_flutter/bloc/categories/categories_event.dart';
import 'package:bookshelf_flutter/bloc/categories/categories_state.dart';
void main() {
group('CategoriesBloc', () {
late CategoriesBloc categoriesBloc;
setUp(() {
categoriesBloc = CategoriesBloc();
});
tearDown(() {
categoriesBloc.close();
});
test('initial state is CategoriesState.initial()', () {
expect(categoriesBloc.state, isA<CategoriesState>());
expect(categoriesBloc.state.isLoading, true);
});
group('LoadCategories', () {
test('loads categories into state', () async {
categoriesBloc.add(const LoadCategories());
await Future.delayed(Duration.zero);
expect(categoriesBloc.state.categories.length, 8);
expect(categoriesBloc.state.filteredCategories.length, 8);
expect(categoriesBloc.state.isLoading, false);
});
});
group('SelectCategory', () {
test('updates selectedCategory in state', () async {
categoriesBloc.add(const LoadCategories());
await Future.delayed(Duration.zero);
final category = categoriesBloc.state.categories.first;
categoriesBloc.add(SelectCategory(category));
await Future.delayed(Duration.zero);
expect(categoriesBloc.state.selectedCategory?.id, category.id);
});
});
group('SearchCategories', () {
test('filters categories by name', () async {
categoriesBloc.add(const LoadCategories());
await Future.delayed(Duration.zero);
categoriesBloc.add(const SearchCategories('Sci'));
await Future.delayed(Duration.zero);
expect(categoriesBloc.state.filteredCategories.length, 1);
expect(categoriesBloc.state.filteredCategories.first.name.toLowerCase(), contains('sci'));
});
test('returns empty list when no matches found', () async {
categoriesBloc.add(const LoadCategories());
await Future.delayed(Duration.zero);
categoriesBloc.add(const SearchCategories('NonExistent'));
await Future.delayed(Duration.zero);
expect(categoriesBloc.state.filteredCategories.length, 0);
});
test('search is case insensitive', () async {
categoriesBloc.add(const LoadCategories());
await Future.delayed(Duration.zero);
categoriesBloc.add(const SearchCategories('classic'));
await Future.delayed(Duration.zero);
expect(categoriesBloc.state.filteredCategories.length, 1);
expect(categoriesBloc.state.filteredCategories.first.name, 'Classic');
});
});
});
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/home/home_bloc.dart';
import 'package:bookshelf_flutter/bloc/home/home_event.dart';
import 'package:bookshelf_flutter/bloc/home/home_state.dart';
import 'package:bookshelf_flutter/models/models.dart';
void main() {
group('HomeBloc', () {
late HomeBloc homeBloc;
setUp(() {
homeBloc = HomeBloc();
});
tearDown(() {
homeBloc.close();
});
test('initial state is HomeState.initial()', () {
expect(homeBloc.state, isA<HomeState>());
expect(homeBloc.state.isLoading, true);
});
group('LoadHomeData', () {
test('loads home data with default screen', () async {
homeBloc.add(const LoadHomeData());
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.library);
expect(homeBloc.state.isLoading, false);
});
});
group('NavigateToScreen', () {
test('navigates to library screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.library));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.library);
});
test('navigates to categories screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.categories));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.categories);
});
test('navigates to wishlist screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.wishlist));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.wishlist);
});
test('navigates to settings screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.settings));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.settings);
});
test('navigates to details screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.details));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.details);
});
test('navigates to add book screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.addBook));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.addBook);
});
test('navigates to scanner screen', () async {
homeBloc.add(const NavigateToScreen(AppScreen.scanner));
await Future.delayed(Duration.zero);
expect(homeBloc.state.currentScreen, AppScreen.scanner);
});
});
});
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/library/library_bloc.dart';
import 'package:bookshelf_flutter/bloc/library/library_event.dart';
import 'package:bookshelf_flutter/bloc/library/library_state.dart';
import 'package:bookshelf_flutter/models/models.dart';
void main() {
group('LibraryBloc', () {
late LibraryBloc libraryBloc;
setUp(() {
libraryBloc = LibraryBloc();
});
tearDown(() {
libraryBloc.close();
});
test('initial state is LibraryState.initial()', () {
expect(libraryBloc.state, isA<LibraryState>());
expect(libraryBloc.state.isLoading, true);
});
group('LoadBooks', () {
test('emits [LibraryState] with books when LoadBooks is added', () async {
final expected = [
isA<LibraryState>()
.having((state) => state.isLoading, 'isLoading', false)
.having((state) => state.books.isNotEmpty, 'books.isNotEmpty', true),
];
expectLater(libraryBloc.stream, emitsInOrder(expected));
libraryBloc.add(const LoadBooks());
});
});
group('SearchBooks', () {
test('filters books by title', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const SearchBooks('Гэтсби'));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.filteredBooks.length, 1);
expect(libraryBloc.state.filteredBooks.first.title, contains('Гэтсби'));
});
test('filters books by author', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const SearchBooks('Оруэлл'));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.filteredBooks.length, 1);
expect(libraryBloc.state.filteredBooks.first.author, contains('Оруэлл'));
});
test('filters books by genre', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const SearchBooks('Fantasy'));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.filteredBooks.isNotEmpty, true);
});
test('returns empty list when no matches found', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const SearchBooks('NonExistent'));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.filteredBooks.length, 0);
});
});
group('BookSelected', () {
test('updates selectedBook in state', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
final book = libraryBloc.state.books.first;
libraryBloc.add(BookSelected(book));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.selectedBook?.id, book.id);
});
});
group('FilterByStatus', () {
test('filters books by status', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const FilterByStatus(BookStatus.reading));
await Future.delayed(Duration.zero);
expect(libraryBloc.state.filteredBooks.every((book) => book.status == BookStatus.reading), true);
expect(libraryBloc.state.statusFilter, BookStatus.reading);
});
});
group('ClearFilters', () {
test('clears search query and shows all books', () async {
libraryBloc.add(const LoadBooks());
await Future.delayed(Duration.zero);
libraryBloc.add(const SearchBooks('test'));
await Future.delayed(Duration.zero);
libraryBloc.add(const ClearFilters());
await Future.delayed(Duration.zero);
expect(libraryBloc.state.searchQuery, '');
expect(libraryBloc.state.filteredBooks.length, libraryBloc.state.books.length);
});
});
});
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/scanner/scanner_bloc.dart';
import 'package:bookshelf_flutter/bloc/scanner/scanner_event.dart';
import 'package:bookshelf_flutter/bloc/scanner/scanner_state.dart';
void main() {
group('ScannerBloc', () {
late ScannerBloc scannerBloc;
setUp(() {
scannerBloc = ScannerBloc();
});
tearDown(() {
scannerBloc.close();
});
test('initial state is ScannerState.initial()', () {
expect(scannerBloc.state, isA<ScannerState>());
expect(scannerBloc.state.isScanning, false);
expect(scannerBloc.state.isProcessing, false);
});
group('StartScanning', () {
test('sets isScanning to true and clears errors', () async {
scannerBloc.add(const StartScanning());
await Future.delayed(Duration.zero);
expect(scannerBloc.state.isScanning, true);
expect(scannerBloc.state.isProcessing, false);
expect(scannerBloc.state.errorMessage, isNull);
});
});
group('StopScanning', () {
test('sets isScanning to false', () async {
scannerBloc.add(const StartScanning());
await Future.delayed(Duration.zero);
scannerBloc.add(const StopScanning());
await Future.delayed(Duration.zero);
expect(scannerBloc.state.isScanning, false);
});
});
group('BookDetected', () {
test('stores detected book data and sets isProcessing to true', () async {
final bookData = {
'title': 'Detected Book',
'author': 'Detected Author',
};
scannerBloc.add(BookDetected(bookData));
await Future.delayed(Duration.zero);
expect(scannerBloc.state.detectedBookData, bookData);
expect(scannerBloc.state.isProcessing, true);
});
});
group('ClearDetectedBook', () {
test('clears detected book data and resets processing', () async {
final bookData = {
'title': 'Detected Book',
'author': 'Detected Author',
};
scannerBloc.add(BookDetected(bookData));
await Future.delayed(Duration.zero);
expect(scannerBloc.state.detectedBookData, isNotNull);
scannerBloc.add(const ClearDetectedBook());
await Future.delayed(Duration.zero);
expect(scannerBloc.state.detectedBookData, isNull);
expect(scannerBloc.state.isProcessing, false);
});
});
});
}

View File

@@ -0,0 +1,105 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/settings/settings_bloc.dart';
import 'package:bookshelf_flutter/bloc/settings/settings_event.dart';
import 'package:bookshelf_flutter/bloc/settings/settings_state.dart';
void main() {
group('SettingsBloc', () {
late SettingsBloc settingsBloc;
setUp(() {
settingsBloc = SettingsBloc();
});
tearDown(() {
settingsBloc.close();
});
test('initial state is SettingsState.initial()', () {
expect(settingsBloc.state, isA<SettingsState>());
expect(settingsBloc.state.isLoading, true);
});
group('LoadSettings', () {
test('loads default settings into state', () async {
settingsBloc.add(const LoadSettings());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.isDarkMode, false);
expect(settingsBloc.state.language, 'English');
expect(settingsBloc.state.notificationsEnabled, true);
expect(settingsBloc.state.isLoading, false);
});
});
group('UpdateTheme', () {
test('updates isDarkMode to true', () async {
settingsBloc.add(const UpdateTheme(true));
await Future.delayed(Duration.zero);
expect(settingsBloc.state.isDarkMode, true);
});
test('updates isDarkMode to false', () async {
settingsBloc.add(const UpdateTheme(true));
await Future.delayed(Duration.zero);
settingsBloc.add(const UpdateTheme(false));
await Future.delayed(Duration.zero);
expect(settingsBloc.state.isDarkMode, false);
});
});
group('UpdateLanguage', () {
test('updates language setting', () async {
settingsBloc.add(const UpdateLanguage('Russian'));
await Future.delayed(Duration.zero);
expect(settingsBloc.state.language, 'Russian');
});
test('updates language to English', () async {
settingsBloc.add(const UpdateLanguage('English'));
await Future.delayed(Duration.zero);
expect(settingsBloc.state.language, 'English');
});
});
group('ToggleNotifications', () {
test('toggles notifications from true to false', () async {
settingsBloc.add(const LoadSettings());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.notificationsEnabled, true);
settingsBloc.add(const ToggleNotifications());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.notificationsEnabled, false);
});
test('toggles notifications from false to true', () async {
settingsBloc.add(const UpdateTheme(false));
await Future.delayed(Duration.zero);
settingsBloc.add(const ToggleNotifications());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.notificationsEnabled, false);
settingsBloc.add(const ToggleNotifications());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.notificationsEnabled, true);
});
});
group('ClearData', () {
test('sets dataCleared to true', () async {
settingsBloc.add(const ClearData());
await Future.delayed(Duration.zero);
expect(settingsBloc.state.dataCleared, true);
});
});
});
}

View File

@@ -0,0 +1,104 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/bloc/wishlist/wishlist_bloc.dart';
import 'package:bookshelf_flutter/bloc/wishlist/wishlist_event.dart';
import 'package:bookshelf_flutter/bloc/wishlist/wishlist_state.dart';
import 'package:bookshelf_flutter/models/models.dart';
void main() {
group('WishlistBloc', () {
late WishlistBloc wishlistBloc;
setUp(() {
wishlistBloc = WishlistBloc();
});
tearDown(() {
wishlistBloc.close();
});
test('initial state is WishlistState.initial()', () {
expect(wishlistBloc.state, isA<WishlistState>());
expect(wishlistBloc.state.isLoading, true);
});
group('LoadWishlist', () {
test('loads wishlist books into state', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
expect(wishlistBloc.state.books.isNotEmpty, true);
expect(wishlistBloc.state.filteredBooks.isNotEmpty, true);
expect(wishlistBloc.state.isLoading, false);
});
});
group('RemoveFromWishlist', () {
test('removes book from both lists', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
final bookId = wishlistBloc.state.books.first.id;
wishlistBloc.add(RemoveFromWishlist(bookId));
await Future.delayed(Duration.zero);
expect(
wishlistBloc.state.books.any((book) => book.id == bookId),
false,
);
expect(
wishlistBloc.state.filteredBooks.any((book) => book.id == bookId),
false,
);
});
});
group('SearchWishlist', () {
test('filters books by title', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
wishlistBloc.add(const SearchWishlist('1984'));
await Future.delayed(Duration.zero);
expect(wishlistBloc.state.filteredBooks.length, 1);
expect(wishlistBloc.state.filteredBooks.first.title, contains('1984'));
});
test('filters books by author', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
wishlistBloc.add(const SearchWishlist('Оруэлл'));
await Future.delayed(Duration.zero);
expect(wishlistBloc.state.filteredBooks.length, 1);
expect(wishlistBloc.state.filteredBooks.first.author, contains('Оруэлл'));
});
test('returns empty list when no matches found', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
wishlistBloc.add(const SearchWishlist('NonExistent'));
await Future.delayed(Duration.zero);
expect(wishlistBloc.state.filteredBooks.length, 0);
});
});
group('MoveToLibrary', () {
test('updates book status to reading', () async {
wishlistBloc.add(const LoadWishlist());
await Future.delayed(Duration.zero);
final bookId = wishlistBloc.state.books.first.id;
wishlistBloc.add(MoveToLibrary(bookId));
await Future.delayed(Duration.zero);
final book = wishlistBloc.state.books.firstWhere((b) => b.id == bookId);
expect(book.status, BookStatus.reading);
expect(wishlistBloc.state.movedBookId, bookId);
});
});
});
}

View File

@@ -0,0 +1,21 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bookshelf_flutter/main.dart';
void main() {
testWidgets('App starts successfully', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const BookshelfApp());
// Verify that the app builds successfully
expect(find.byType(MaterialApp), findsOneWidget);
});
}