feat(app): 搜索改为调用后端并渲染卡片结果
This commit is contained in:
1 parent
730e38622f
commit
ade0cc3566
13 files changed
+424
-20
No files matched your search
@@ -0,0 +1,51 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
import 'package:fruitshorts/core/data/remote/catalog_api.dart';
|
||||
|
||||
import '../support/fake_http_adapter.dart';
|
||||
|
||||
void main() {
|
||||
test('should_request_search_with_keyword_and_paging_when_searching', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(options) async => FakeHttpResponse(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'message': 'ok',
|
||||
'data': {
|
||||
'total': 1,
|
||||
'list': [
|
||||
{
|
||||
'id': '2111012753262120962',
|
||||
'title': 'Velvet Nights',
|
||||
'synopsis': 'A city romance',
|
||||
'cover': '/files/velvet.png',
|
||||
'genre': 'Romance',
|
||||
'totalEpisodes': 12,
|
||||
'isHot': true,
|
||||
'language': 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
final api = CatalogApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final page = await api.search('velvet');
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.uri.path, '/app/search');
|
||||
expect(request.queryParameters['keyword'], 'velvet');
|
||||
expect(request.queryParameters['pageIndex'], '1');
|
||||
expect(request.queryParameters['pageSize'], '20');
|
||||
expect(page.total, 1);
|
||||
expect(page.list.single.title, 'Velvet Nights');
|
||||
expect(page.list.single.language, 'id');
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/home_feed.dart';
|
||||
import 'package:fruitshorts/core/data/models/search_result.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/search_repository.dart';
|
||||
|
||||
/// 测试用目录数据:三部分区里的短剧,标题与后端演示数据保持一致。
|
||||
class FakeCatalogRepository implements CatalogRepository {
|
||||
class FakeCatalogRepository implements CatalogRepository, SearchRepository {
|
||||
FakeCatalogRepository({List<Drama>? dramas, this.banners = const []})
|
||||
: dramas = dramas ?? demoDramas;
|
||||
|
||||
@@ -21,6 +23,20 @@ class FakeCatalogRepository implements CatalogRepository {
|
||||
return dramas.firstWhere((drama) => drama.id == dramaId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SearchResult> search(
|
||||
String keyword, {
|
||||
int pageIndex = 1,
|
||||
int pageSize = 20,
|
||||
}) async {
|
||||
final query = keyword.trim().toLowerCase();
|
||||
final hits = dramas.where((drama) {
|
||||
final haystack = [drama.title, drama.synopsis, drama.genre].join(' ');
|
||||
return haystack.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
return SearchResult(total: hits.length, list: hits);
|
||||
}
|
||||
|
||||
HomeFeed _feed() => HomeFeed(
|
||||
banners: banners,
|
||||
sections: [FeedSection(code: 'hot', title: '热门推荐', dramas: dramas)],
|
||||
@@ -45,6 +61,7 @@ Drama _drama(String id, String title, String genre, {required int format}) =>
|
||||
synopsis: '$title synopsis',
|
||||
posterUrl: '/files/$id.png',
|
||||
genre: genre,
|
||||
language: 'id',
|
||||
detailLoaded: true,
|
||||
episodes: [
|
||||
for (var index = 0; index < 7; index++)
|
||||
|
||||
Reference in new issue
Block a user