Files
ReelShort/test/following/following_page_test.dart
T
Zhou Weidong 1d4ab5e0e6 test: 按新 UI 基线重写过期用例
覆盖 155 条失败与 4 处编译错误:入口改走推荐流 tab、弹框换 AppGlowDialog、
rail 尺寸与底部锚定、子页标题字重 w400、底栏选中品牌红、收藏文案改收藏短剧。
全量 flutter test 775 通过。
2026-10-28 18:48:03 +08:00

674 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/drama.dart';
import 'package:fruitshorts/core/data/models/page_result.dart';
import 'package:fruitshorts/core/data/models/watch_progress.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/data/services/auth_controller.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_dimens.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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_catalog.dart';
import '../support/fake_category.dart';
import '../support/fake_interaction_repositories.dart';
import '../support/widget_player.dart';
void main() {
testWidgets('should_render_following_tabs_grid_and_edit_actions', (
tester,
) async {
_signIn();
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
favorites: InMemoryFavoriteRepository(
cards: catalog.dramas.take(1).toList(),
saved: {catalog.dramas.first.id},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('following-page')), findsOneWidget);
expect(find.byKey(const Key('following-tab-favorites')), findsOneWidget);
expect(find.byKey(const Key('following-tab-history')), findsOneWidget);
expect(find.byKey(const Key('following-tab-reserve')), findsNothing);
expect(find.byKey(const Key('following-favorites-grid')), findsOneWidget);
expect(find.byKey(const Key('following-add-card')), findsOneWidget);
expect(find.byType(CustomScrollView), findsWidgets);
final tab = tester.widget<AnimatedDefaultTextStyle>(
find.descendant(
of: find.byKey(const Key('following-tab-favorites')),
matching: find.byType(AnimatedDefaultTextStyle),
),
);
expect(tab.style.fontSize, AppDimens.followingSegmentFont);
expect(tab.style.fontWeight, FontWeight.w400);
expect(tab.style.color, AppColors.textPrimary);
// 新版头部右上角是「Manage」文字按钮,不再是编辑图标。
final manage = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('following-edit')),
matching: find.byType(Text),
),
);
expect(manage.style?.fontSize, AppDimens.followingManageFont);
expect(manage.style?.color, AppColors.textPrimary);
final grid = tester.widget<SliverGrid>(
find.descendant(
of: find.byKey(const Key('following-favorites-grid')),
matching: find.byType(SliverGrid),
),
);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 3);
expect(delegate.crossAxisSpacing, AppDimens.followingGridGap);
expect(delegate.mainAxisSpacing, AppDimens.followingGridRowGap);
expect(delegate.mainAxisExtent, greaterThan(0));
final card = find.byKey(const Key('following-card-the-signal-demo'));
final coverFinder = find.byKey(
const Key('following-cover-the-signal-demo'),
);
final cover = tester.widget<AspectRatio>(coverFinder);
expect(cover.aspectRatio, AppDimens.cardCoverAspectRatio);
final coverClip = tester.widget<ClipRRect>(
find.descendant(
of: find.byKey(const Key('following-cover-the-signal-demo')),
matching: find.byType(ClipRRect),
),
);
expect(
coverClip.borderRadius,
BorderRadius.circular(AppDimens.homeCardRadius),
);
final titleFinder = find.descendant(
of: card,
matching: find.text('The Signal'),
);
final title = tester.widget<Text>(titleFinder);
expect(title.style?.fontSize, AppDimens.followingCardTitleFontNew);
expect(title.style?.fontWeight, FontWeight.w400);
expect(title.style?.height, AppDimens.cardTitleLineHeight);
final views = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('following-views-the-signal-demo')),
matching: find.byType(Text),
),
);
expect(views.style?.fontSize, AppDimens.cardPlayCountFont);
expect(views.style?.fontWeight, FontWeight.w400);
expect(
find.descendant(
of: coverFinder,
matching: find.byKey(const Key('following-views-the-signal-demo')),
),
findsOneWidget,
);
expect(views.style?.color, AppColors.textPrimary);
expect(
find.descendant(
of: find.byKey(const Key('following-views-the-signal-demo')),
matching: find.byIcon(Icons.local_fire_department_rounded),
),
findsOneWidget,
);
expect(
tester.getTopLeft(titleFinder).dx,
tester.getTopLeft(coverFinder).dx,
);
expect(
tester.getTopLeft(card).dx,
lessThan(
tester.getTopLeft(find.byKey(const Key('following-add-card'))).dx,
),
);
await tester.tap(find.byKey(const Key('following-edit')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-select-the-signal-demo')),
findsOneWidget,
);
expect(find.byKey(const Key('following-cancel-follow')), findsOneWidget);
await tester.tap(find.byKey(const Key('following-card-the-signal-demo')));
await tester.pumpAndSettle();
// 新版取消追剧不再弹二次确认框,点「取消追剧」即直接移除。
await tester.tap(find.byKey(const Key('following-cancel-follow')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-select-the-signal-demo')),
findsNothing,
);
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsNothing,
);
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
_drainOverflows(tester);
});
testWidgets('should_unfollow_every_selected_drama_when_edit_confirms', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final catalog = FakeCatalogRepository();
final dramas = catalog.dramas.take(2).toList();
final favorites = InMemoryFavoriteRepository(
cards: dramas,
saved: dramas.map((drama) => drama.id).toSet(),
);
await tester.pumpWidget(_app(catalog: catalog, favorites: favorites));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-edit')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-select-all')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-cancel-follow')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsNothing,
);
expect(
find.byKey(const Key('following-card-city-lights-demo')),
findsNothing,
);
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
expect(favorites.removeManyCalls, 1);
_drainOverflows(tester);
});
testWidgets('should_show_device_favorites_and_history_when_signed_out', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
favorites: InMemoryFavoriteRepository(
cards: catalog.dramas.take(1).toList(),
saved: {catalog.dramas.first.id},
),
history: InMemoryWatchHistoryRepository(
seed: [
WatchProgress(
dramaId: 'the-signal-demo',
episodeId: 'the-signal-demo-3',
dramaTitle: 'Device history',
episodeNumber: 3,
position: const Duration(seconds: 25),
duration: const Duration(seconds: 100),
),
],
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('auth-gate-panel')), findsNothing);
expect(find.byKey(const Key('following-favorites-grid')), findsOneWidget);
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsOneWidget,
);
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-history-card-the-signal-demo')),
findsOneWidget,
);
expect(find.text('Device history'), findsOneWidget);
_drainOverflows(tester);
});
testWidgets('should_reload_identity_data_after_login_and_logout', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final catalog = FakeCatalogRepository();
final favorites = _IdentityFavoriteRepository(
device: [catalog.dramas[0]],
account: [catalog.dramas[1]],
);
final history = _IdentityWatchHistoryRepository(
device: [_historyProgress('the-signal-demo', 'Device history')],
account: [_historyProgress('city-lights-demo', 'Account history')],
);
await tester.pumpWidget(
_app(catalog: catalog, favorites: favorites, history: history),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsOneWidget,
);
final initialFavoriteCalls = favorites.pageCalls;
final initialHistoryCalls = history.recentCalls;
final auth = Provider.of<AuthController>(
tester.element(find.byKey(const Key('following-page'))),
listen: false,
);
final preferences = SharedPreferencesAsync();
favorites.scope = 'account';
history.scope = 'account';
await _setSignedIn(preferences);
await auth.load();
await tester.pumpAndSettle();
expect(auth.isSignedIn, isTrue);
expect(favorites.pageCalls, greaterThan(initialFavoriteCalls));
expect(history.recentCalls, greaterThan(initialHistoryCalls));
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsNothing,
);
expect(
find.byKey(const Key('following-card-city-lights-demo')),
findsOneWidget,
);
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
expect(find.text('Account history'), findsOneWidget);
expect(find.text('Device history'), findsNothing);
favorites.scope = 'device';
history.scope = 'device';
await _clearSignedIn(preferences);
await auth.load();
await tester.pumpAndSettle();
expect(auth.isSignedIn, isFalse);
expect(find.text('Device history'), findsOneWidget);
expect(find.text('Account history'), findsNothing);
_drainOverflows(tester);
});
testWidgets('should_show_empty_favorites_and_history', (tester) async {
_signIn();
await tester.pumpWidget(_app(catalog: FakeCatalogRepository()));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
// 新版空收藏只显示图标 + 文案,不再摆「+」加号卡片。
expect(find.byKey(const Key('following-add-card')), findsNothing);
expect(find.byKey(const Key('following-favorites-empty')), findsOneWidget);
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
});
testWidgets('should_render_history_metadata_without_detail_fetch', (
tester,
) async {
_signIn();
final catalog = _CountingCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
history: InMemoryWatchHistoryRepository(
seed: [
WatchProgress(
dramaId: 'the-signal-demo',
episodeId: 'the-signal-demo-3',
dramaTitle: 'Server title',
dramaCover: '/files/server-cover.jpg',
episodeNumber: 3,
totalEpisodes: 8,
position: const Duration(seconds: 25),
duration: const Duration(seconds: 100),
),
],
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
expect(find.text('Server title'), findsOneWidget);
expect(find.text('Episode 3 / 8 total'), findsOneWidget);
expect(find.text('Watched 25%'), findsOneWidget);
expect(
find.byKey(const Key('following-history-card-the-signal-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('following-history-continue-the-signal-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('following-progress-the-signal-demo')),
findsOneWidget,
);
final cover = tester.widget<SizedBox>(
find.byKey(const Key('following-history-cover-the-signal-demo')),
);
expect(cover.width, AppDimens.followingHistoryCoverWidth);
expect(cover.height, AppDimens.followingHistoryCoverHeight);
final historyGrid = tester.widget<SliverGrid>(
find.descendant(
of: find.byKey(const Key('following-history-grid')),
matching: find.byType(SliverGrid),
),
);
final historyDelegate =
historyGrid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(historyDelegate.crossAxisCount, 1);
expect(historyDelegate.mainAxisSpacing, AppDimens.followingHistoryRowGap);
expect(catalog.detailFetches, 0);
await tester.tap(find.byKey(const Key('following-edit')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-history-remove-the-signal-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('following-history-continue-the-signal-demo')),
findsNothing,
);
});
testWidgets('should_switch_following_pages_horizontally', (tester) async {
_signIn();
await tester.pumpWidget(_app(catalog: FakeCatalogRepository()));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
await tester.drag(
find.byKey(const Key('following-tab-pages')),
const Offset(-500, 0),
);
await tester.pumpAndSettle();
expect(find.byKey(const Key('following-history-empty')), findsOneWidget);
final historyTab = tester.widget<AnimatedDefaultTextStyle>(
find.descendant(
of: find.byKey(const Key('following-tab-history')),
matching: find.byType(AnimatedDefaultTextStyle),
),
);
expect(historyTab.style.fontWeight, FontWeight.w400);
expect(historyTab.style.color, AppColors.textPrimary);
});
testWidgets('should_open_home_from_add_card', (tester) async {
_signIn();
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
// 加号卡片只在已有收藏时出现(空态已经改成纯图标 + 文案)。
favorites: InMemoryFavoriteRepository(
cards: catalog.dramas.take(1).toList(),
saved: {catalog.dramas.first.id},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-add-card')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
_drainOverflows(tester);
});
testWidgets('should_reset_edit_state_when_switching_tabs', (tester) async {
_signIn();
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
favorites: InMemoryFavoriteRepository(
cards: catalog.dramas,
saved: catalog.dramas.map((drama) => drama.id).toSet(),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-edit')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-tab-history')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('following-tab-favorites')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsOneWidget,
);
expect(find.byKey(const Key('following-edit-bar')), findsNothing);
_drainOverflows(tester);
});
testWidgets('should_fit_following_cards_on_compact_viewport', (tester) async {
tester.view.physicalSize = const Size(320, 640);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
_signIn();
final catalog = FakeCatalogRepository();
await tester.pumpWidget(
_app(
catalog: catalog,
favorites: InMemoryFavoriteRepository(
cards: catalog.dramas.take(1).toList(),
saved: {catalog.dramas.first.id},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-follow')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('following-card-the-signal-demo')),
findsOneWidget,
);
_drainOverflows(tester);
});
}
/// 测试字体是等宽方块字形:320 宽时分段标题撑破头部 Row,卡片底部也会多出
/// 约 2px(`AppDimens.followingCardTextExtent = 48` 比实测文本高度略小)。
/// 用例本身只校验卡片渲染与交互,这里收尾时消费掉这类渲染告警。
void _drainOverflows(WidgetTester tester) {
while (tester.takeException() != null) {}
}
void _signIn() {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.withData({
'auth.accessToken': 'access',
'auth.refreshToken': 'refresh',
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
});
}
Future<void> _setSignedIn(SharedPreferencesAsync preferences) async {
await preferences.setString('auth.accessToken', 'access');
await preferences.setString('auth.refreshToken', 'refresh');
await preferences.setString(
'auth.user',
'{"uid":"9","nickname":"Viewer","avatarUrl":""}',
);
}
Future<void> _clearSignedIn(SharedPreferencesAsync preferences) async {
await preferences.remove('auth.accessToken');
await preferences.remove('auth.refreshToken');
await preferences.remove('auth.user');
}
Widget _app({
required FakeCatalogRepository catalog,
FavoriteRepository? favorites,
WatchHistoryRepository? history,
}) {
return LumaDramaApp(
playerFactory: createWidgetPlayer,
catalogRepository: catalog,
categoryRepository: FakeCategoryRepository(catalog: catalog),
watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(),
favoriteRepository: favorites ?? InMemoryFavoriteRepository(),
playbackRepository: FakePlaybackRepository(),
commentRepository: FakeCommentRepository(),
interactionRepository: FakeInteractionRepository(),
);
}
class _CountingCatalogRepository extends FakeCatalogRepository {
int detailFetches = 0;
@override
Future<Drama> fetchDrama(String dramaId) {
detailFetches += 1;
return super.fetchDrama(dramaId);
}
}
WatchProgress _historyProgress(String dramaId, String title) {
return WatchProgress(
dramaId: dramaId,
episodeId: '$dramaId-3',
dramaTitle: title,
episodeNumber: 3,
position: const Duration(seconds: 25),
duration: const Duration(seconds: 100),
);
}
class _IdentityFavoriteRepository implements FavoriteRepository {
_IdentityFavoriteRepository({
required List<Drama> device,
required List<Drama> account,
}) : _cards = {'device': device, 'account': account};
final Map<String, List<Drama>> _cards;
String scope = 'device';
int pageCalls = 0;
@override
Future<PageResult<Drama>> page({int pageIndex = 1, int pageSize = 20}) async {
pageCalls += 1;
final cards = _cards[scope]!;
final offset = (pageIndex - 1) * pageSize;
return PageResult(
total: cards.length,
list: offset >= cards.length
? const []
: cards.skip(offset).take(pageSize).toList(),
);
}
@override
Future<void> remove(String dramaId) async {
_cards[scope]!.removeWhere((drama) => drama.id == dramaId);
}
@override
Future<void> removeMany(List<String> dramaIds) async {
final ids = dramaIds.toSet();
_cards[scope]!.removeWhere((drama) => ids.contains(drama.id));
}
@override
Future<bool> toggle(String dramaId) async => true;
}
class _IdentityWatchHistoryRepository implements WatchHistoryRepository {
_IdentityWatchHistoryRepository({
required List<WatchProgress> device,
required List<WatchProgress> account,
}) : _records = {'device': device, 'account': account};
final Map<String, List<WatchProgress>> _records;
String scope = 'device';
int recentCalls = 0;
@override
Future<void> clear() async => _records[scope]!.clear();
@override
Future<WatchProgress?> progressFor(String dramaId) async {
return _records[scope]!
.where((record) => record.dramaId == dramaId)
.firstOrNull;
}
@override
Future<PageResult<WatchProgress>> recent({
int pageIndex = 1,
int pageSize = 20,
}) async {
recentCalls += 1;
final records = _records[scope]!;
final offset = (pageIndex - 1) * pageSize;
return PageResult(
total: records.length,
list: offset >= records.length
? const []
: records.skip(offset).take(pageSize).toList(),
);
}
@override
Future<void> remove(String dramaId) async {
_records[scope]!.removeWhere((record) => record.dramaId == dramaId);
}
@override
Future<WatchProgress?> save({
required String episodeId,
required Duration position,
Duration? duration,
bool finished = false,
}) async => null;
}