feat(home): 首页按内容类型分频道 Feed 并支持分页
This commit is contained in:
1 parent
4d0be47ee3
commit
ae61d9ec4c
22 files changed
+1001
-71
No files matched your search
@@ -4,6 +4,7 @@ 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/catalog_controller.dart';
|
||||
@@ -136,6 +137,13 @@ class ScriptedCatalogRepository implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => feed;
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async => const FeedPage();
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async {
|
||||
detailCalls++;
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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() {
|
||||
String envelope(Object? data) =>
|
||||
jsonEncode({'code': 0, 'message': 'ok', 'data': data});
|
||||
|
||||
test('should_send_types_and_cursor_when_feed_query_has_filters', () async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope({
|
||||
'list': [
|
||||
{
|
||||
'id': '11',
|
||||
'title': 'Anime',
|
||||
'cover': '/files/anime.png',
|
||||
'preview': {
|
||||
'id': '12',
|
||||
'number': 1,
|
||||
'title': 'Episode 1',
|
||||
'isFree': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
'nextCursor': 10,
|
||||
'hasMore': true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
final api = CatalogApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final page = await api.feed(
|
||||
types: const ['ANIME', 'LIVE'],
|
||||
cursor: '9',
|
||||
size: 20,
|
||||
);
|
||||
|
||||
expect(adapter.requests.single.uri.path, '/app/feed');
|
||||
expect(adapter.requests.single.queryParameters, {
|
||||
'types': 'ANIME,LIVE',
|
||||
'cursor': '9',
|
||||
'size': '20',
|
||||
});
|
||||
expect(page.dramas.single.id, '11');
|
||||
expect(page.nextCursor, '10');
|
||||
expect(page.hasMore, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'should_omit_empty_types_and_treat_null_cursor_string_as_empty',
|
||||
() async {
|
||||
final adapter = FakeHttpAdapter(
|
||||
(_) async => FakeHttpResponse(
|
||||
envelope({'list': [], 'nextCursor': 'null', 'hasMore': false}),
|
||||
),
|
||||
);
|
||||
final api = CatalogApi(
|
||||
ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter),
|
||||
);
|
||||
|
||||
final page = await api.feed();
|
||||
|
||||
expect(adapter.requests.single.queryParameters, {'size': '10'});
|
||||
expect(page.dramas, isEmpty);
|
||||
expect(page.nextCursor, isNull);
|
||||
expect(page.hasMore, isFalse);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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/home_feed_controller.dart';
|
||||
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'should_seed_recommend_without_requesting_when_initial_feed_is_ready',
|
||||
() async {
|
||||
final repository = _ScriptedFeedRepository();
|
||||
final controller = HomeFeedController(repository);
|
||||
|
||||
controller.seedRecommend([_drama('seed')]);
|
||||
|
||||
expect(controller.channel, HomeFeedChannel.recommend);
|
||||
expect(controller.dramas.map((drama) => drama.id), ['seed']);
|
||||
expect(controller.loaded, isTrue);
|
||||
expect(repository.calls, isEmpty);
|
||||
controller.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('should_request_anime_live_and_recommend_with_expected_types', () async {
|
||||
final repository = _ScriptedFeedRepository()
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('anime')]))
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('live')]))
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('recommend')]));
|
||||
final controller = HomeFeedController(repository);
|
||||
controller.seedRecommend([_drama('seed')]);
|
||||
|
||||
await controller.selectChannel(HomeFeedChannel.anime);
|
||||
await controller.selectChannel(HomeFeedChannel.live);
|
||||
await controller.selectChannel(HomeFeedChannel.recommend);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(repository.calls.map((call) => call.types), [
|
||||
['ANIME'],
|
||||
['LIVE'],
|
||||
[],
|
||||
]);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test(
|
||||
'should_keep_cached_data_while_silently_refreshing_cached_channel',
|
||||
() async {
|
||||
final refresh = Completer<FeedPage>();
|
||||
final repository = _ScriptedFeedRepository()
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('anime-old')]))
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('live')]))
|
||||
..enqueue(() => refresh.future);
|
||||
final controller = HomeFeedController(repository);
|
||||
|
||||
await controller.selectChannel(HomeFeedChannel.anime);
|
||||
await controller.selectChannel(HomeFeedChannel.live);
|
||||
await controller.selectChannel(HomeFeedChannel.anime);
|
||||
|
||||
expect(controller.dramas.single.id, 'anime-old');
|
||||
refresh.complete(FeedPage(dramas: [_drama('anime-new')]));
|
||||
await pumpEventQueue();
|
||||
expect(controller.dramas.single.id, 'anime-new');
|
||||
controller.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test('should_append_unique_items_and_stop_when_has_more_is_false', () async {
|
||||
final repository = _ScriptedFeedRepository()
|
||||
..enqueue(
|
||||
() async => FeedPage(
|
||||
dramas: [_drama('a'), _drama('b')],
|
||||
nextCursor: 'next',
|
||||
hasMore: true,
|
||||
),
|
||||
)
|
||||
..enqueue(
|
||||
() async =>
|
||||
FeedPage(dramas: [_drama('b'), _drama('c')], hasMore: false),
|
||||
);
|
||||
final controller = HomeFeedController(repository);
|
||||
await controller.selectChannel(HomeFeedChannel.anime);
|
||||
|
||||
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.cursor, 'next');
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('should_ignore_stale_response_when_channels_switch_quickly', () async {
|
||||
final anime = Completer<FeedPage>();
|
||||
final live = Completer<FeedPage>();
|
||||
final repository = _ScriptedFeedRepository()
|
||||
..enqueue(() => anime.future)
|
||||
..enqueue(() => live.future);
|
||||
final controller = HomeFeedController(repository);
|
||||
|
||||
final animeLoad = controller.selectChannel(HomeFeedChannel.anime);
|
||||
final liveLoad = controller.selectChannel(HomeFeedChannel.live);
|
||||
live.complete(FeedPage(dramas: [_drama('live')]));
|
||||
await liveLoad;
|
||||
anime.complete(FeedPage(dramas: [_drama('anime')]));
|
||||
await animeLoad;
|
||||
|
||||
expect(controller.channel, HomeFeedChannel.live);
|
||||
expect(controller.dramas.single.id, 'live');
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('should_keep_old_data_when_refresh_fails', () async {
|
||||
final repository = _ScriptedFeedRepository()
|
||||
..enqueue(() async => FeedPage(dramas: [_drama('old')]))
|
||||
..enqueue(() async => throw StateError('offline'));
|
||||
final controller = HomeFeedController(repository);
|
||||
await controller.selectChannel(HomeFeedChannel.anime);
|
||||
|
||||
final refreshed = await controller.refreshCurrent();
|
||||
|
||||
expect(refreshed, isFalse);
|
||||
expect(controller.dramas.single.id, 'old');
|
||||
expect(controller.lastError, contains('offline'));
|
||||
controller.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
class FeedCall {
|
||||
const FeedCall({
|
||||
required this.types,
|
||||
required this.cursor,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final List<String> types;
|
||||
final String? cursor;
|
||||
final int size;
|
||||
}
|
||||
|
||||
class _ScriptedFeedRepository implements CatalogRepository {
|
||||
final List<FeedCall> 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? cursor,
|
||||
int size = 10,
|
||||
}) {
|
||||
calls.add(FeedCall(types: types, 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: '');
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
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/catalog_controller.dart';
|
||||
@@ -59,6 +60,13 @@ class _FakeCatalog implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => const HomeFeed();
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async => const FeedPage();
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async => _drama;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_emit_channel_callbacks_and_render_selected_channel', (
|
||||
tester,
|
||||
) async {
|
||||
final selected = <HomeFeedChannel>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(),
|
||||
locale: const Locale('en'),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: HomeFeedHeader(
|
||||
l10n: AppLocalizations.of(context)!,
|
||||
channel: HomeFeedChannel.anime,
|
||||
onChannelSelected: selected.add,
|
||||
onRefresh: () async => true,
|
||||
onSearch: () {},
|
||||
onMore: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('home-feed-tab-anime')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-tab-real')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-tab-recommend')), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Anime')).style?.fontWeight,
|
||||
FontWeight.w800,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(const Key('home-feed-tab-real')));
|
||||
await tester.tap(find.byKey(const Key('home-feed-tab-recommend')));
|
||||
await tester.tap(find.byKey(const Key('home-feed-tab-anime')));
|
||||
|
||||
expect(selected, [
|
||||
HomeFeedChannel.live,
|
||||
HomeFeedChannel.recommend,
|
||||
HomeFeedChannel.anime,
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/home_feed_header.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
@@ -439,6 +440,8 @@ void main() {
|
||||
builder: (context) => Scaffold(
|
||||
body: HomeFeedHeader(
|
||||
l10n: AppLocalizations.of(context)!,
|
||||
channel: HomeFeedChannel.recommend,
|
||||
onChannelSelected: (_) {},
|
||||
onRefresh: () {
|
||||
calls++;
|
||||
return completer.future;
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:shared_preferences_platform_interface/shared_preferences_async_p
|
||||
import 'package:fruitshorts/app.dart';
|
||||
import 'package:fruitshorts/core/config/app_config.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/repositories/favorite_repository.dart';
|
||||
@@ -158,6 +159,13 @@ class _EmptyCatalogRepository implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => const HomeFeed();
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async => const FeedPage();
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async => _feed.dramas.first;
|
||||
}
|
||||
@@ -177,6 +185,13 @@ class _PendingCatalogRepository implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => _feed;
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async => FeedPage(dramas: _feed.dramas);
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async => _feed.dramas.first;
|
||||
}
|
||||
@@ -116,12 +116,18 @@ void main() {
|
||||
expect(player.homeMode, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('should_show_coming_soon_for_anime_tab', (tester) async {
|
||||
testWidgets('should_switch_to_anime_channel', (tester) async {
|
||||
await _pumpApp(tester);
|
||||
|
||||
await tester.tap(find.byKey(const Key('home-feed-tab-anime')));
|
||||
await tester.pump();
|
||||
expect(find.text('Coming soon'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Coming soon'), findsNothing);
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Anime')).style?.fontWeight,
|
||||
FontWeight.w800,
|
||||
);
|
||||
expect(find.byKey(const Key('home-feed-title')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_refresh_feed_without_blank_loading', (tester) async {
|
||||
|
||||
@@ -412,6 +412,120 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('updateFeed', () {
|
||||
test('should_append_feed_without_restarting_current_drama', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final feed = [_drama('signal'), _drama('city')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.attach(
|
||||
PlayerPage(
|
||||
drama: feed.first,
|
||||
initialIndex: 0,
|
||||
homeMode: true,
|
||||
feedDramas: feed,
|
||||
),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(factory: factory.create, coordinator: PlaybackCoordinator());
|
||||
await _drain(logic);
|
||||
final activeBefore = logic.session.active;
|
||||
|
||||
logic.updateFeed([...feed, _drama('third')], keepDramaId: 'signal');
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(logic.currentDramaId, 'signal');
|
||||
expect(logic.visibleIndices, [0, 1, 2]);
|
||||
expect(logic.sessionEpisodes, hasLength(3));
|
||||
expect(logic.session.active, same(activeBefore));
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('should_remap_current_drama_without_restarting_when_order_changes',
|
||||
() async {
|
||||
final factory = _RecordingFactory();
|
||||
final feed = [_drama('signal'), _drama('city')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.attach(
|
||||
PlayerPage(
|
||||
drama: feed.first,
|
||||
initialIndex: 1,
|
||||
homeMode: true,
|
||||
feedDramas: feed,
|
||||
),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(factory: factory.create, coordinator: PlaybackCoordinator());
|
||||
await _drain(logic);
|
||||
final activeBefore = logic.session.active;
|
||||
|
||||
logic.updateFeed(
|
||||
[_drama('city'), _drama('signal'), _drama('third')],
|
||||
keepDramaId: 'city',
|
||||
);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(logic.currentDramaId, 'city');
|
||||
expect(logic.session.currentIndex, 0);
|
||||
expect(logic.session.active, same(activeBefore));
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('should_return_to_first_drama_when_feed_channel_changes', () async {
|
||||
final factory = _RecordingFactory();
|
||||
final feed = [_drama('signal'), _drama('city')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.attach(
|
||||
PlayerPage(
|
||||
drama: feed.first,
|
||||
initialIndex: 0,
|
||||
homeMode: true,
|
||||
feedDramas: feed,
|
||||
),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(factory: factory.create, coordinator: PlaybackCoordinator());
|
||||
await _drain(logic);
|
||||
|
||||
logic.updateFeed([_drama('anime-a'), _drama('anime-b')]);
|
||||
await _drain(logic);
|
||||
|
||||
expect(logic.queue.entryAt(0).drama.id, 'anime-a');
|
||||
expect(logic.currentDramaId, 'anime-a');
|
||||
expect(logic.session.currentIndex, 0);
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
test('should_clear_queue_safely_when_feed_becomes_empty', () async {
|
||||
final feed = [_drama('signal')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.attach(
|
||||
PlayerPage(
|
||||
drama: feed.first,
|
||||
initialIndex: 0,
|
||||
homeMode: true,
|
||||
feedDramas: feed,
|
||||
),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(
|
||||
factory: _RecordingFactory().create,
|
||||
coordinator: PlaybackCoordinator(),
|
||||
);
|
||||
await _drain(logic);
|
||||
|
||||
logic.updateFeed(const []);
|
||||
|
||||
expect(logic.queue.length, 0);
|
||||
expect(logic.visibleIndices, isEmpty);
|
||||
expect(logic.currentDramaId, isNull);
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
});
|
||||
|
||||
group('lifecycle', () {
|
||||
test('should_pause_active_player_when_backgrounded_and_play_it_when_resumed', () async {
|
||||
final factory = _RecordingFactory();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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/models/search_result.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
|
||||
@@ -18,6 +19,23 @@ class FakeCatalogRepository implements CatalogRepository, SearchRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => _feed();
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async {
|
||||
final filtered = types.isEmpty
|
||||
? dramas
|
||||
: dramas.where((drama) => _matchesFeedTypes(drama, types)).toList();
|
||||
return FeedPage(dramas: filtered.take(size).toList());
|
||||
}
|
||||
|
||||
bool _matchesFeedTypes(Drama drama, List<String> types) {
|
||||
final dramaType = drama.id == 'the-signal-demo' ? 'ANIME' : 'LIVE';
|
||||
return types.contains(dramaType);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async {
|
||||
return dramas.firstWhere((drama) => drama.id == dramaId);
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/models/watch_progress.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/catalog_repository.dart';
|
||||
@@ -265,6 +266,13 @@ class ScriptedWatchCatalog implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => const HomeFeed();
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) async => const FeedPage();
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async {
|
||||
calls.add(dramaId);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/models/watch_progress.dart';
|
||||
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||
@@ -271,6 +272,13 @@ class _FailingWatchCatalog implements CatalogRepository {
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() => _delegate.fetchDiscoverFeed();
|
||||
|
||||
@override
|
||||
Future<FeedPage> fetchFeed({
|
||||
List<String> types = const [],
|
||||
String? cursor,
|
||||
int size = 10,
|
||||
}) => _delegate.fetchFeed(types: types, cursor: cursor, size: size);
|
||||
|
||||
@override
|
||||
Future<Drama> fetchDrama(String dramaId) async {
|
||||
if (dramaId == 'broken') throw StateError('missing detail');
|
||||
|
||||
Reference in new issue
Block a user