feat(app): 搜索改为调用后端并渲染卡片结果

This commit is contained in:
Zhou Weidong committed 2026-10-20 04:34:12 +08:00
1 parent 730e38622f
commit ade0cc3566
13 files changed
+424 -20

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/search_result.dart';
import 'package:fruitshorts/core/data/remote/api_exception.dart';
import 'package:fruitshorts/core/data/repositories/search_repository.dart';
import 'package:fruitshorts/pages/discover/discover_logic.dart';
import '../support/fake_watch_history.dart';
Drama _drama(String id, {String language = 'id'}) => Drama(
id: id,
title: id,
synopsis: 'synopsis',
posterUrl: '/files/$id.png',
language: language,
episodes: const [],
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('should_query_backend_and_render_results_when_search_text_changes', () async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
logic.search.text = 'velvet';
logic.onSearchChanged();
await pumpEventQueue();
expect(repository.calls, [(keyword: 'velvet', pageIndex: 1, pageSize: 20)]);
expect(logic.searchLoading, isFalse);
expect(logic.visibleDramas(const []).single.id, 'velvet');
logic.dispose();
});
test('should_keep_server_results_when_text_only_loses_whitespace', () async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
logic.search.text = 'velvet ';
logic.onSearchChanged();
await pumpEventQueue();
logic.search.text = 'velvet';
logic.onSearchChanged();
await pumpEventQueue();
expect(repository.calls, hasLength(1));
expect(logic.visibleDramas(const []).single.id, 'velvet');
logic.dispose();
});
test('should_expose_error_message_when_search_fails', () async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: _FakeSearchRepository(
error: const ApiException(400, 'Keyword is invalid'),
),
)..searchDebounce = Duration.zero;
logic.search.text = '%%%';
logic.onSearchChanged();
await pumpEventQueue();
expect(logic.searchError, 'Keyword is invalid');
expect(logic.searchLoading, isFalse);
logic.dispose();
});
test('should_clear_server_results_when_search_text_is_empty', () async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
logic.search.text = 'velvet';
logic.onSearchChanged();
await pumpEventQueue();
logic.search.clear();
logic.onSearchChanged();
expect(logic.visibleDramas([_drama('local')]).single.id, 'local');
logic.dispose();
});
}
class _FakeSearchRepository implements SearchRepository {
_FakeSearchRepository({this.results = const [], this.error});
final List<Drama> results;
final ApiException? error;
final List<({String keyword, int pageIndex, int pageSize})> calls = [];
@override
Future<SearchResult> search(
String keyword, {
int pageIndex = 1,
int pageSize = 20,
}) async {
calls.add((keyword: keyword, pageIndex: pageIndex, pageSize: pageSize));
if (error != null) throw error!;
return SearchResult(total: results.length, list: results);
}
}
@@ -428,6 +428,7 @@ Future<void> _pumpApp(
LumaDramaApp(
playerFactory: createWidgetPlayer,
catalogRepository: catalog,
searchRepository: catalog,
categoryRepository: categoryRepository,
watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(),
favoriteRepository: InMemoryFavoriteRepository(),