diff --git a/lib/src/features/search/presentation/pages/search_page.dart b/lib/src/features/search/presentation/pages/search_page.dart index ca902a7d0..691da04af 100644 --- a/lib/src/features/search/presentation/pages/search_page.dart +++ b/lib/src/features/search/presentation/pages/search_page.dart @@ -53,9 +53,16 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi initializePreferences(); scrollController.addListener(onScroll); - // Initialize search type based on whether we're searching within a community + // Initialize community-scoped search defaults. if (widget.community != null) { - context.read().add(const SearchFiltersUpdated(searchType: MetaSearchType.posts)); + final community = widget.community!; + context.read().add( + SearchFiltersUpdated( + searchType: MetaSearchType.posts, + communityFilter: community.id, + communityFilterName: community.name, + ), + ); WidgetsBinding.instance.addPostFrameCallback((_) => searchTextFieldFocus.requestFocus()); } diff --git a/lib/src/features/search/presentation/state/search_bloc.dart b/lib/src/features/search/presentation/state/search_bloc.dart index b89c218fd..188f0690f 100644 --- a/lib/src/features/search/presentation/state/search_bloc.dart +++ b/lib/src/features/search/presentation/state/search_bloc.dart @@ -362,17 +362,17 @@ class SearchBloc extends Bloc { void _onFiltersUpdated(SearchFiltersUpdated event, Emitter emit) { emit( state.copyWith( - searchSortType: event.sortType, - sortTypeIcon: event.sortTypeIcon, - sortTypeLabel: event.sortTypeLabel, + searchSortType: event.sortType ?? _searchUnset, + sortTypeIcon: event.sortTypeIcon ?? _searchUnset, + sortTypeLabel: event.sortTypeLabel ?? _searchUnset, searchType: event.searchType, feedListType: event.feedListType, searchByUrl: event.searchByUrl, - communityFilter: event.communityFilter, - communityFilterName: event.communityFilterName, + communityFilter: event.communityFilter ?? _searchUnset, + communityFilterName: event.communityFilterName ?? _searchUnset, clearCommunityFilter: event.clearCommunityFilter, - creatorFilter: event.creatorFilter, - creatorFilterName: event.creatorFilterName, + creatorFilter: event.creatorFilter ?? _searchUnset, + creatorFilterName: event.creatorFilterName ?? _searchUnset, clearCreatorFilter: event.clearCreatorFilter, ), ); diff --git a/test/features/search/search_bloc_test.dart b/test/features/search/search_bloc_test.dart new file mode 100644 index 000000000..3a2278e33 --- /dev/null +++ b/test/features/search/search_bloc_test.dart @@ -0,0 +1,283 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:thunder/src/features/account/account.dart'; +import 'package:thunder/src/features/comment/comment.dart'; +import 'package:thunder/src/features/community/community.dart'; +import 'package:thunder/src/features/instance/instance.dart'; +import 'package:thunder/src/features/search/search.dart'; +import 'package:thunder/src/features/user/user.dart'; +import 'package:thunder/src/foundation/persistence/persistence.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + +class _MockCommentRepository extends Mock implements CommentRepository {} + +class _MockSearchRepository extends Mock implements SearchRepository {} + +class _MockCommunityRepository extends Mock implements CommunityRepository {} + +class _MockUserRepository extends Mock implements UserRepository {} + +class _MockInstanceRepository extends Mock implements InstanceRepository {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const account = Account(id: '1', index: 0, instance: 'lemmy.world'); + const communityId = 42; + + setUpAll(() async { + SharedPreferences.setMockInitialValues({}); + await UserPreferences.instance.initialize(); + }); + + group('SearchBloc', () { + late _MockCommentRepository commentRepository; + late _MockSearchRepository searchRepository; + late _MockCommunityRepository communityRepository; + late _MockUserRepository userRepository; + late _MockInstanceRepository instanceRepository; + + setUp(() { + commentRepository = _MockCommentRepository(); + searchRepository = _MockSearchRepository(); + communityRepository = _MockCommunityRepository(); + userRepository = _MockUserRepository(); + instanceRepository = _MockInstanceRepository(); + }); + + blocTest( + 'passes community filter when searching posts', + build: () { + when( + () => searchRepository.search( + query: 'thunder', + type: MetaSearchType.posts, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).thenAnswer( + (_) async => const SearchResults( + type: MetaSearchType.posts, + comments: [], + posts: [], + communities: [], + users: [], + ), + ); + + return SearchBloc( + account: account, + commentRepository: commentRepository, + searchRepository: searchRepository, + communityRepository: communityRepository, + userRepository: userRepository, + instanceRepository: instanceRepository, + ); + }, + act: (bloc) { + bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.posts, communityFilter: communityId)); + bloc.add(const SearchStarted(query: 'thunder')); + }, + expect: () => [ + isA().having((state) => state.searchType, 'searchType', MetaSearchType.posts).having((state) => state.communityFilter, 'communityFilter', communityId), + isA().having((state) => state.status, 'status', SearchStatus.loading), + isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), + ], + verify: (_) { + verify( + () => searchRepository.search( + query: 'thunder', + type: MetaSearchType.posts, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).called(1); + }, + ); + + blocTest( + 'passes community filter when searching comments', + build: () { + when( + () => searchRepository.search( + query: 'thunder', + type: MetaSearchType.comments, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).thenAnswer( + (_) async => const SearchResults( + type: MetaSearchType.comments, + comments: [], + posts: [], + communities: [], + users: [], + ), + ); + + return SearchBloc( + account: account, + commentRepository: commentRepository, + searchRepository: searchRepository, + communityRepository: communityRepository, + userRepository: userRepository, + instanceRepository: instanceRepository, + ); + }, + act: (bloc) { + bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.comments, communityFilter: communityId)); + bloc.add(const SearchStarted(query: 'thunder')); + }, + expect: () => [ + isA().having((state) => state.searchType, 'searchType', MetaSearchType.comments).having((state) => state.communityFilter, 'communityFilter', communityId), + isA().having((state) => state.status, 'status', SearchStatus.loading), + isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), + ], + verify: (_) { + verify( + () => searchRepository.search( + query: 'thunder', + type: MetaSearchType.comments, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).called(1); + }, + ); + + blocTest( + 'keeps community filter when switching from posts to comments', + build: () { + when( + () => searchRepository.search( + query: 'post query', + type: MetaSearchType.posts, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).thenAnswer( + (_) async => const SearchResults( + type: MetaSearchType.posts, + comments: [], + posts: [], + communities: [], + users: [], + ), + ); + + when( + () => searchRepository.search( + query: 'comment query', + type: MetaSearchType.comments, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).thenAnswer( + (_) async => const SearchResults( + type: MetaSearchType.comments, + comments: [], + posts: [], + communities: [], + users: [], + ), + ); + + return SearchBloc( + account: account, + commentRepository: commentRepository, + searchRepository: searchRepository, + communityRepository: communityRepository, + userRepository: userRepository, + instanceRepository: instanceRepository, + ); + }, + act: (bloc) async { + bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.posts, communityFilter: communityId)); + bloc.add(const SearchStarted(query: 'post query')); + + await Future.delayed(Duration.zero); + + bloc.add(const SearchFiltersUpdated(searchType: MetaSearchType.comments)); + bloc.add(const SearchStarted(query: 'comment query')); + }, + expect: () => [ + isA().having((state) => state.searchType, 'searchType', MetaSearchType.posts).having((state) => state.communityFilter, 'communityFilter', communityId), + isA().having((state) => state.status, 'status', SearchStatus.loading), + isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), + isA().having((state) => state.searchType, 'searchType', MetaSearchType.comments).having((state) => state.communityFilter, 'communityFilter', communityId), + isA().having((state) => state.status, 'status', SearchStatus.loading), + isA().having((state) => state.status, 'status', SearchStatus.success).having((state) => state.communityFilter, 'communityFilter', communityId), + ], + verify: (_) { + verify( + () => searchRepository.search( + query: 'post query', + type: MetaSearchType.posts, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).called(1); + + verify( + () => searchRepository.search( + query: 'comment query', + type: MetaSearchType.comments, + sort: SearchSortType.topYear, + listingType: FeedListType.all, + limit: searchResultsPerPage, + page: 1, + communityId: communityId, + creatorId: null, + minimumUpvotes: null, + nsfw: null, + ), + ).called(1); + }, + ); + }); +}