feat(theater): 剧场分类列表服务端分页

This commit is contained in:
Zhou Weidong committed 2026-10-21 18:24:14 +08:00
1 parent f8769f2e1d
commit 13a14f3e85
16 files changed
+769 -21

No files matched your search

@@ -298,6 +298,7 @@ class _ScriptedFeedRepository implements CatalogRepository {
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) {
@@ -0,0 +1,180 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/feed_page.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
import 'package:fruitshorts/core/data/services/theater_feed_controller.dart';
void main() {
test('should_request_all_and_selected_category_with_expected_ids', () async {
final repository = _ScriptedTheaterRepository()
..enqueue(() async => FeedPage(dramas: [_drama('all')]))
..enqueue(() async => FeedPage(dramas: [_drama('category')]));
final controller = TheaterFeedController(repository);
await controller.select(null);
await controller.select('7');
expect(repository.calls.map((call) => call.categoryId), [null, '7']);
expect(controller.dramas.single.id, 'category');
controller.dispose();
});
test('should_keep_cached_category_visible_while_refreshing', () async {
final refresh = Completer<FeedPage>();
final repository = _ScriptedTheaterRepository()
..enqueue(() async => FeedPage(dramas: [_drama('all-old')]))
..enqueue(() async => FeedPage(dramas: [_drama('category')]))
..enqueue(() => refresh.future);
final controller = TheaterFeedController(repository);
await controller.select(null);
await controller.select('7');
await controller.select(null);
expect(controller.dramas.single.id, 'all-old');
refresh.complete(FeedPage(dramas: [_drama('all-new')]));
await pumpEventQueue();
expect(controller.dramas.single.id, 'all-new');
controller.dispose();
});
test('should_append_unique_items_and_stop_when_has_more_is_false', () async {
final repository = _ScriptedTheaterRepository()
..enqueue(
() async => FeedPage(
dramas: [_drama('a'), _drama('b')],
nextCursor: 'next',
hasMore: true,
),
)
..enqueue(
() async =>
FeedPage(dramas: [_drama('b'), _drama('c')], hasMore: false),
);
final controller = TheaterFeedController(repository);
await controller.select('7');
await controller.loadMore();
await controller.loadMore();
expect(controller.dramas.map((drama) => drama.id), ['a', 'b', 'c']);
expect(repository.calls, hasLength(2));
expect(repository.calls.last.categoryId, '7');
expect(repository.calls.last.cursor, 'next');
controller.dispose();
});
test('should_keep_old_data_when_refresh_fails', () async {
final repository = _ScriptedTheaterRepository()
..enqueue(() async => FeedPage(dramas: [_drama('old')]))
..enqueue(() async => throw StateError('offline'));
final controller = TheaterFeedController(repository);
await controller.select('7');
final refreshed = await controller.refreshCurrent();
expect(refreshed, isFalse);
expect(controller.dramas.single.id, 'old');
expect(controller.lastError, contains('offline'));
controller.dispose();
});
test(
'should_retry_automatically_when_returning_to_failed_category',
() async {
final repository = _ScriptedTheaterRepository()
..enqueue(() async => FeedPage(dramas: [_drama('all')]))
..enqueue(() async => throw StateError('offline'))
..enqueue(() async => FeedPage(dramas: [_drama('category-8')]))
..enqueue(() async => FeedPage(dramas: [_drama('category-7-retry')]));
final controller = TheaterFeedController(repository);
await controller.select(null);
await controller.select('7');
expect(controller.lastError, contains('offline'));
await controller.select('8');
await controller.select('7');
expect(controller.dramas.single.id, 'category-7-retry');
expect(repository.calls, hasLength(4));
controller.dispose();
},
);
test('should_finish_loading_with_error_when_request_times_out', () async {
final pending = Completer<FeedPage>();
final repository = _ScriptedTheaterRepository()
..enqueue(() => pending.future);
final controller = TheaterFeedController(
repository,
requestTimeout: const Duration(milliseconds: 10),
);
await controller.select(null);
expect(controller.loadingFirstPage, isFalse);
expect(controller.loaded, isTrue);
expect(controller.lastError, isNotNull);
controller.dispose();
});
test('should_not_report_first_page_loading_without_request', () {
final controller = TheaterFeedController(_ScriptedTheaterRepository());
expect(controller.loaded, isFalse);
expect(controller.loadingFirstPage, isFalse);
controller.dispose();
});
}
class TheaterFeedCall {
const TheaterFeedCall({
required this.categoryId,
required this.cursor,
required this.size,
});
final String? categoryId;
final String? cursor;
final int size;
}
class _ScriptedTheaterRepository implements CatalogRepository {
final List<TheaterFeedCall> calls = [];
final List<Future<FeedPage> Function()> _responses = [];
void enqueue(Future<FeedPage> Function() response) {
_responses.add(response);
}
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) {
calls.add(
TheaterFeedCall(categoryId: categoryId, cursor: cursor, size: size),
);
return _responses.removeAt(0)();
}
@override
Future<HomeFeed> fetchHomeFeed() async => const HomeFeed();
@override
Future<HomeFeed> fetchDiscoverFeed() async => const HomeFeed();
@override
Future<Drama> fetchDrama(String dramaId) async => _drama(dramaId);
}
Drama _drama(String id) =>
Drama(id: id, title: id, synopsis: id, posterUrl: '');