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

+248 -5
View File
@@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/app.dart';
import 'package:fruitshorts/core/data/models/content_category.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/category_repository.dart';
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
@@ -72,11 +73,20 @@ void main() {
testWidgets('should_filter_grid_when_category_chip_is_tapped', (
tester,
) async {
await _pumpTheater(tester, categoryRepository: _GridCategoryRepository());
final catalog = _CategoryTheaterCatalog();
await _pumpTheater(
tester,
catalogRepository: catalog,
categoryRepository: _GridCategoryRepository(),
);
await tester.tap(find.byKey(const Key('theater-category-7')));
await tester.pumpAndSettle();
expect(
catalog.requestedCategoryIds.where((categoryId) => categoryId == '7'),
hasLength(1),
);
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
@@ -92,6 +102,9 @@ void main() {
) async {
await _pumpTheater(
tester,
catalogRepository: _SinglePageTheaterCatalog(
dramas: _tallDiscoverFeed.dramas,
),
categoryRepository: _GridCategoryRepository(
discoverFeed: _tallDiscoverFeed,
),
@@ -121,6 +134,7 @@ void main() {
testWidgets('should_show_empty_and_retry_states', (tester) async {
await _pumpTheater(
tester,
catalogRepository: _SinglePageTheaterCatalog(dramas: const []),
categoryRepository: _GridCategoryRepository(
discoverFeed: const HomeFeed(),
),
@@ -144,6 +158,18 @@ void main() {
expect(find.textContaining('discover feed unavailable'), findsOneWidget);
});
testWidgets('should_show_retry_when_theater_feed_fails', (tester) async {
await _pumpTheater(
tester,
catalogRepository: _FailingTheaterCatalog(),
categoryRepository: _GridCategoryRepository(),
);
expect(find.byKey(const Key('home-categories-error')), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
expect(find.textContaining('theater feed unavailable'), findsWidgets);
});
testWidgets('should_show_search_results_when_text_changes', (tester) async {
await _pumpTheater(tester, categoryRepository: _GridCategoryRepository());
@@ -161,19 +187,63 @@ void main() {
);
});
testWidgets('should_load_next_theater_page_near_scroll_bottom', (
tester,
) async {
final catalog = _PagedTheaterCatalog([
FeedPage(dramas: _pageDramas(0, 20), nextCursor: '20', hasMore: true),
FeedPage(dramas: _pageDramas(20, 20)),
]);
await _pumpTheater(
tester,
catalogRepository: catalog,
categoryRepository: _GridCategoryRepository(),
);
final position = tester
.state<ScrollableState>(
find
.descendant(
of: find.byKey(const Key('discover-refresh')),
matching: find.byType(Scrollable),
)
.first,
)
.position;
position.jumpTo(position.maxScrollExtent);
await tester.pump();
await tester.pumpAndSettle();
expect(catalog.calls, 2);
expect(catalog.requestedCursors, [null, '20']);
expect(find.byKey(const Key('home-drama-card-page-39')), findsOneWidget);
});
testWidgets('should_refresh_feed_when_pulled_down', (tester) async {
final counter = _CountingCategoryRepository(
_GridCategoryRepository(discoverFeed: _tallDiscoverFeed),
);
await _pumpTheater(tester, categoryRepository: counter);
final theaterCounter = _CountingTheaterCatalog(
dramas: _tallDiscoverFeed.dramas,
);
await _pumpTheater(
tester,
catalogRepository: theaterCounter,
categoryRepository: counter,
);
final before = counter.categoryLoads + counter.feedLoads;
final beforeCategories = counter.categoryLoads + counter.feedLoads;
final beforeTheater = theaterCounter.feedCalls;
await tester.drag(_verticalFeed, const Offset(0, 320));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
expect(counter.categoryLoads + counter.feedLoads, greaterThan(before));
expect(
counter.categoryLoads + counter.feedLoads,
greaterThan(beforeCategories),
);
expect(theaterCounter.feedCalls, beforeTheater + 1);
expect(find.byType(RefreshProgressIndicator), findsNothing);
});
@@ -205,6 +275,9 @@ void main() {
testWidgets('should_pin_theater_filters_when_scrolling', (tester) async {
await _pumpTheater(
tester,
catalogRepository: _SinglePageTheaterCatalog(
dramas: _tallDiscoverFeed.dramas,
),
categoryRepository: _GridCategoryRepository(
discoverFeed: _tallDiscoverFeed,
),
@@ -243,6 +316,9 @@ void main() {
testWidgets('should_keep_category_row_stable_across_scrolls', (tester) async {
await _pumpTheater(
tester,
catalogRepository: _SinglePageTheaterCatalog(
dramas: _tallDiscoverFeed.dramas,
),
categoryRepository: _GridCategoryRepository(
discoverFeed: _tallDiscoverFeed,
),
@@ -357,12 +433,13 @@ final Finder _verticalFeed = find.byWidgetPredicate(
Future<void> _pumpTheater(
WidgetTester tester, {
required CategoryRepository categoryRepository,
FakeCatalogRepository? catalogRepository,
}) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final catalog = FakeCatalogRepository();
final catalog = catalogRepository ?? FakeCatalogRepository();
await tester.pumpWidget(
LumaDramaApp(
playerFactory: createWidgetPlayer,
@@ -469,3 +546,169 @@ class _CountingCategoryRepository implements CategoryRepository {
return delegate.fetchDiscoverFeed();
}
}
class _TheaterOnlyCatalog extends FakeCatalogRepository {
_TheaterOnlyCatalog({super.dramas});
final FakeCatalogRepository _homeDelegate = FakeCatalogRepository();
@override
Future<HomeFeed> fetchHomeFeed() => _homeDelegate.fetchHomeFeed();
@override
Future<HomeFeed> fetchDiscoverFeed() => _homeDelegate.fetchDiscoverFeed();
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) {
return _homeDelegate.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
}
class _SinglePageTheaterCatalog extends _TheaterOnlyCatalog {
_SinglePageTheaterCatalog({required List<Drama> dramas})
: super(dramas: dramas);
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) async {
if (size != 20) {
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
return FeedPage(dramas: dramas.take(size).toList());
}
}
class _CategoryTheaterCatalog extends _TheaterOnlyCatalog {
final List<String?> requestedCategoryIds = [];
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) async {
if (size != 20) {
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
requestedCategoryIds.add(categoryId);
final rows = categoryId == '7'
? dramas.where((drama) => drama.id == 'city-lights-demo').toList()
: dramas;
return FeedPage(dramas: rows.take(size).toList());
}
}
class _CountingTheaterCatalog extends _TheaterOnlyCatalog {
_CountingTheaterCatalog({required List<Drama> dramas})
: super(dramas: dramas);
int feedCalls = 0;
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) {
if (size != 20) {
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
feedCalls++;
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
}
class _PagedTheaterCatalog extends _TheaterOnlyCatalog {
_PagedTheaterCatalog(this.pages);
final List<FeedPage> pages;
final List<String?> requestedCursors = [];
int get calls => requestedCursors.length;
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) async {
if (size != 20) {
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
requestedCursors.add(cursor);
return pages.removeAt(0);
}
}
class _FailingTheaterCatalog extends _TheaterOnlyCatalog {
@override
Future<FeedPage> fetchFeed({
List<String> types = const [],
String? categoryId,
String? cursor,
int size = 10,
}) {
if (size == 20) {
return Future.error(StateError('theater feed unavailable'));
}
return super.fetchFeed(
types: types,
categoryId: categoryId,
cursor: cursor,
size: size,
);
}
}
List<Drama> _pageDramas(int start, int count) => [
for (var index = start; index < start + count; index++)
Drama(
id: 'page-$index',
title: 'Page $index',
synopsis: 'Page $index synopsis',
posterUrl: '',
genre: 'Drama',
),
];