feat(flutter): 接入分类记录收藏与下载授权

This commit is contained in:
Zhou Weidong committed 2026-10-17 14:49:16 +08:00
1 parent 00280ca0da
commit cc4d7c5a9d
52 files changed
+4440 -171

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
// Author: fruit.com.cn
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/content_category.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart';
import 'package:fruitshorts/core/data/remote/category_api.dart';
import 'package:fruitshorts/core/data/repositories/category_repository.dart';
import 'package:fruitshorts/core/data/services/category_controller.dart';
import '../support/fake_http_adapter.dart';
Drama _drama(String id) => Drama(
id: id,
title: 'Drama $id',
synopsis: 'synopsis',
posterUrl: '/files/$id.png',
);
void main() {
test(
'should_keep_snowflake_category_id_as_string_when_api_returns_list',
() async {
const id = '2111012753262120962';
final adapter = FakeHttpAdapter(
(_) async => FakeHttpResponse(
jsonEncode({
'code': 0,
'message': 'ok',
'data': [
{'id': id, 'name': 'Romance'},
],
}),
),
);
final client = ApiClient(
baseUrl: 'http://127.0.0.1:8081',
adapter: adapter,
);
final categories = await CategoryApi(client).list();
expect(categories.single.id, id);
expect(categories.single.name, 'Romance');
expect(adapter.requests.single.uri.path, '/app/categories');
},
);
test(
'should_map_category_id_to_discover_section_when_category_is_selected',
() async {
final repository = ScriptedCategoryRepository(
categories: const [
ContentCategory(id: '7', name: 'Romance'),
ContentCategory(id: '8', name: 'Action'),
],
feed: HomeFeed(
sections: [
FeedSection(code: 'hot', title: 'Hot', dramas: [_drama('hot-1')]),
FeedSection(code: 'new', title: 'New', dramas: [_drama('new-1')]),
FeedSection(
code: 'category-7',
title: 'Romance',
dramas: [_drama('romance-1')],
),
],
),
);
final controller = CategoryController(repository);
await controller.load();
controller.selectCategory('7');
expect(controller.selectedCode, 'category-7');
expect(controller.visibleDramas.map((drama) => drama.id), ['romance-1']);
expect(controller.selectedSectionMissing, isFalse);
controller.selectCategory('8');
expect(controller.selectedCode, 'category-8');
expect(controller.visibleDramas, isEmpty);
expect(controller.selectedSectionMissing, isTrue);
},
);
test(
'should_keep_hot_and_new_entries_when_discover_feed_provides_them',
() async {
final repository = ScriptedCategoryRepository(
feed: HomeFeed(
sections: [
FeedSection(code: 'hot', title: 'Hot', dramas: [_drama('hot-1')]),
FeedSection(code: 'new', title: 'New', dramas: [_drama('new-1')]),
FeedSection(
code: 'category-7',
title: 'Romance',
dramas: [_drama('romance-1')],
),
],
),
);
final controller = CategoryController(repository);
await controller.load();
controller.selectCode(CategoryController.hotCode);
expect(controller.visibleDramas.map((drama) => drama.id), ['hot-1']);
controller.selectCode(CategoryController.newCode);
expect(controller.visibleDramas.map((drama) => drama.id), ['new-1']);
},
);
test('should_dedupe_dramas_when_all_categories_are_selected', () async {
final repository = ScriptedCategoryRepository(
feed: HomeFeed(
sections: [
FeedSection(code: 'hot', title: 'Hot', dramas: [_drama('same')]),
FeedSection(
code: 'category-7',
title: 'Romance',
dramas: [_drama('same'), _drama('other')],
),
],
),
);
final controller = CategoryController(repository);
await controller.load();
expect(controller.selectedCode, CategoryController.allCode);
expect(controller.visibleDramas.map((drama) => drama.id), [
'same',
'other',
]);
});
test('should_clear_error_and_retry_when_initial_load_fails', () async {
final repository = ScriptedCategoryRepository(fail: true);
final controller = CategoryController(repository);
await controller.load();
expect(controller.error, 'categoriesError');
expect(controller.loading, isFalse);
repository.fail = false;
await controller.load();
expect(controller.error, isNull);
expect(repository.loads, 2);
});
}
class ScriptedCategoryRepository implements CategoryRepository {
ScriptedCategoryRepository({
this.categories = const [],
this.feed = const HomeFeed(),
this.fail = false,
});
List<ContentCategory> categories;
HomeFeed feed;
bool fail;
int loads = 0;
@override
Future<List<ContentCategory>> fetchCategories() async {
loads++;
if (fail) throw StateError('offline');
return categories;
}
@override
Future<HomeFeed> fetchDiscoverFeed() async {
if (fail) throw StateError('offline');
return feed;
}
}