328 lines
11 KiB
Dart
328 lines
11 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'package:flutter/material.dart';
|
|
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/catalog_repository.dart';
|
|
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
|
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_dimens.dart';
|
|
import 'package:fruitshorts/pages/home/widgets/home_feed_card.dart';
|
|
import 'package:fruitshorts/pages/home/widgets/home_feed_grid.dart';
|
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
|
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
|
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
|
|
|
import '../support/fake_category.dart';
|
|
import '../support/fake_haptics.dart';
|
|
import '../support/fake_interaction_repositories.dart';
|
|
import '../support/widget_player.dart';
|
|
|
|
const _categories = [
|
|
ContentCategory(id: 'costume', name: '古装'),
|
|
ContentCategory(id: 'community', name: '社群'),
|
|
ContentCategory(id: 'uplifting', name: '爽剧'),
|
|
ContentCategory(id: 'sweet', name: '甜宠'),
|
|
ContentCategory(id: 'face-slap', name: '打脸虐渣'),
|
|
ContentCategory(id: 'angst', name: '虐恋'),
|
|
];
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.empty();
|
|
});
|
|
|
|
testWidgets('should_render_primary_sections_and_secondary_tags_in_order', (
|
|
tester,
|
|
) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
final sectionKeys = [
|
|
const Key('home-section-category-古装'),
|
|
const Key('home-section-category-社群'),
|
|
const Key('home-section-category-爽剧'),
|
|
const Key('home-section-category-甜宠'),
|
|
const Key('home-section-category-打脸虐渣'),
|
|
const Key('home-section-category-虐恋'),
|
|
];
|
|
final sectionCenters = sectionKeys
|
|
.map((key) => tester.getCenter(find.byKey(key)).dx)
|
|
.toList();
|
|
expect(sectionCenters, orderedEquals(sectionCenters.toList()..sort()));
|
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
|
expect(find.text('Featured'), findsNothing);
|
|
expect(find.text('New'), findsNothing);
|
|
expect(find.text('Categories'), findsNothing);
|
|
expect(find.text('Charts'), findsNothing);
|
|
expect(find.text('古装'), findsOneWidget);
|
|
expect(find.text('社群'), findsOneWidget);
|
|
expect(find.byKey(const Key('mine-mini-player')), findsNothing);
|
|
expect(find.byType(FloatingActionButton), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_hide_unmatched_content_sections', (tester) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(
|
|
tester,
|
|
repository,
|
|
categories: const [ContentCategory(id: 'romance', name: 'Romance')],
|
|
);
|
|
|
|
expect(find.byKey(const Key('home-section-category-古装')), findsNothing);
|
|
expect(find.byKey(const Key('home-section-category-社群')), findsNothing);
|
|
expect(find.byKey(const Key('home-tag-uplifting')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_switch_home_sections_without_pushing_pages', (
|
|
tester,
|
|
) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
String firstDramaId() {
|
|
return tester
|
|
.widget<HomeFeedCard>(find.byType(HomeFeedCard).first)
|
|
.drama
|
|
.id;
|
|
}
|
|
|
|
expect(find.byKey(const Key('home-grid')), findsOneWidget);
|
|
expect(find.byKey(const Key('home-section-featured')), findsNothing);
|
|
expect(firstDramaId(), 'page1-0');
|
|
|
|
await tester.tap(find.byKey(const Key('home-section-category-古装')));
|
|
await tester.pumpAndSettle();
|
|
expect(firstDramaId(), 'page1-0');
|
|
|
|
await tester.tap(find.byKey(const Key('home-section-category-社群')));
|
|
await tester.pumpAndSettle();
|
|
expect(firstDramaId(), 'page1-0');
|
|
expect(find.byKey(const Key('filter-page')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_keep_scroll_position_when_switching_sections', (
|
|
tester,
|
|
) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
await tester.drag(
|
|
find.byKey(const Key('home-grid-scroll')),
|
|
const Offset(0, -320),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
final scrollable = find
|
|
.descendant(
|
|
of: find.byKey(const Key('home-grid-scroll')),
|
|
matching: find.byType(Scrollable),
|
|
)
|
|
.first;
|
|
final before = tester.state<ScrollableState>(scrollable).position.pixels;
|
|
expect(before, greaterThan(0));
|
|
|
|
await tester.tap(find.byKey(const Key('home-section-category-社群')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('home-section-category-古装')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
|
|
|
final afterScrollable = find
|
|
.descendant(
|
|
of: find.byKey(const Key('home-grid-scroll')),
|
|
matching: find.byType(Scrollable),
|
|
)
|
|
.first;
|
|
expect(
|
|
tester.state<ScrollableState>(afterScrollable).position.pixels,
|
|
closeTo(before, 0.1),
|
|
);
|
|
});
|
|
|
|
testWidgets('should_swipe_between_primary_sections', (tester) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
|
await tester.drag(
|
|
find.byKey(const Key('home-primary-page-view')),
|
|
const Offset(-320, 0),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
|
final selected = tester.widget<AnimatedDefaultTextStyle>(
|
|
find.byKey(const Key('home-primary-label-community')),
|
|
);
|
|
expect(selected.style.fontSize, AppDimens.homePrimaryTabSelectedFont);
|
|
expect(selected.style.fontWeight, FontWeight.w700);
|
|
|
|
await tester.drag(
|
|
find.byKey(const Key('home-primary-page-view')),
|
|
const Offset(320, 0),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('home-secondary-tags')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_load_next_page_without_expanding_page_size', (
|
|
tester,
|
|
) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
expect(find.byKey(const Key('home-feed-card-page1-0')), findsOneWidget);
|
|
await tester.drag(
|
|
find.byKey(const Key('home-grid-scroll')),
|
|
const Offset(0, -1200),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(repository.cursors, contains('page1'));
|
|
expect(repository.requestedSizes.every((size) => size <= 20), isTrue);
|
|
|
|
expect(find.byKey(const Key('home-grid-refresh')), findsOneWidget);
|
|
final gridScrollable = find
|
|
.descendant(
|
|
of: find.byKey(const Key('home-grid-scroll')),
|
|
matching: find.byType(Scrollable),
|
|
);
|
|
expect(gridScrollable, findsOneWidget);
|
|
final secondPageCard = find.byKey(
|
|
const Key('home-feed-card-page2-0'),
|
|
);
|
|
final feed = tester
|
|
.widget<HomeFeedGrid>(find.byType(HomeFeedGrid))
|
|
.feedController;
|
|
expect(
|
|
feed.dramas.map((drama) => drama.id),
|
|
contains('page2-0'),
|
|
);
|
|
final position = tester.state<ScrollableState>(gridScrollable).position;
|
|
await tester.pump();
|
|
position.jumpTo(position.maxScrollExtent);
|
|
await tester.pump();
|
|
position.jumpTo(position.maxScrollExtent);
|
|
await tester.pump();
|
|
expect(secondPageCard, findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_open_player_when_grid_card_is_tapped', (tester) async {
|
|
final repository = _PagedCatalogRepository();
|
|
await _pumpApp(tester, repository);
|
|
|
|
await tester.tap(find.byKey(const Key('home-feed-card-page1-0')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final player = tester.widget<PlayerPage>(find.byType(PlayerPage).last);
|
|
expect(player.homeMode, isFalse);
|
|
});
|
|
}
|
|
|
|
Future<void> _pumpApp(
|
|
WidgetTester tester,
|
|
_PagedCatalogRepository repository, {
|
|
List<ContentCategory> categories = _categories,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
await tester.pumpWidget(
|
|
LumaDramaApp(
|
|
playerFactory: createWidgetPlayer,
|
|
catalogRepository: repository,
|
|
categoryRepository: FakeCategoryRepository(
|
|
catalog: repository,
|
|
categories: categories,
|
|
),
|
|
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
|
favoriteRepository: InMemoryFavoriteRepository(
|
|
cards: repository.allDramas,
|
|
),
|
|
playbackRepository: FakePlaybackRepository(),
|
|
commentRepository: FakeCommentRepository(),
|
|
interactionRepository: FakeInteractionRepository(),
|
|
haptics: FakeHaptics(),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
class _PagedCatalogRepository implements CatalogRepository {
|
|
_PagedCatalogRepository()
|
|
: pageOne = List.generate(30, (index) => _drama('page1-$index')),
|
|
pageTwo = List.generate(2, (index) => _drama('page2-$index'));
|
|
|
|
final List<Drama> pageOne;
|
|
final List<Drama> pageTwo;
|
|
final List<String?> cursors = [];
|
|
final List<int> requestedSizes = [];
|
|
|
|
List<Drama> get allDramas => [...pageOne, ...pageTwo];
|
|
|
|
@override
|
|
Future<HomeFeed> fetchHomeFeed() async => _feed(pageOne);
|
|
|
|
@override
|
|
Future<HomeFeed> fetchDiscoverFeed() async => _feed(pageOne);
|
|
|
|
@override
|
|
Future<FeedPage> fetchFeed({
|
|
List<String> types = const [],
|
|
String? categoryId,
|
|
String? cursor,
|
|
int size = 10,
|
|
}) async {
|
|
requestedSizes.add(size);
|
|
if (types.isNotEmpty) {
|
|
return const FeedPage(dramas: []);
|
|
}
|
|
cursors.add(cursor);
|
|
if (cursor == 'page1') {
|
|
return FeedPage(dramas: pageTwo, hasMore: false);
|
|
}
|
|
return FeedPage(dramas: pageOne, nextCursor: 'page1', hasMore: true);
|
|
}
|
|
|
|
@override
|
|
Future<Drama> fetchDrama(String dramaId) async {
|
|
return allDramas.firstWhere((drama) => drama.id == dramaId);
|
|
}
|
|
|
|
HomeFeed _feed(List<Drama> dramas) {
|
|
return HomeFeed(
|
|
sections: [FeedSection(code: 'hot', title: 'Hot', dramas: dramas)],
|
|
);
|
|
}
|
|
}
|
|
|
|
Drama _drama(String id) {
|
|
final index = int.tryParse(id.split('-').last) ?? 0;
|
|
return Drama(
|
|
id: id,
|
|
title: 'Grid Drama $id',
|
|
synopsis: 'synopsis',
|
|
posterUrl: '',
|
|
genre: 'Romance',
|
|
category: index.isEven ? '古装' : 'Urban',
|
|
viewCount: (10 - index) * 10000,
|
|
createdAt: DateTime(2026, 10, 1).add(Duration(days: index)),
|
|
detailLoaded: true,
|
|
episodes: [
|
|
Episode(
|
|
id: '$id-1',
|
|
number: 1,
|
|
title: 'Episode 1',
|
|
isFree: true,
|
|
videoUrl: 'https://example.test/$id.mp4',
|
|
),
|
|
],
|
|
);
|
|
}
|