From bfba2b7c72aa7e3041c21c3c9c6be26bb82cc657 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 15:03:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(flutter):=20=E5=89=A7=E5=9C=BA=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=A1=86=E6=94=B9=E7=99=BD=E5=BA=95=E5=B9=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=A6=9C=E5=8D=95=E6=96=B0=E5=89=A7=E9=A2=84=E7=BA=A6?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/widgets/plain_page_scaffold.dart | 80 +++++++ lib/l10n/app_en.arb | 10 + lib/l10n/app_id.arb | 5 + lib/l10n/app_localizations.dart | 30 +++ lib/l10n/app_localizations_en.dart | 17 ++ lib/l10n/app_localizations_id.dart | 17 ++ lib/l10n/app_localizations_zh.dart | 17 ++ lib/l10n/app_zh.arb | 5 + lib/pages/discover/discover_view.dart | 15 +- .../widgets/discover_search_field.dart | 2 +- lib/pages/new_releases/new_releases_page.dart | 43 ++++ lib/pages/ranking/ranking_page.dart | 196 ++++++++++++++++++ lib/pages/reserve/reserve_page.dart | 48 +++++ test/discover/home_all_dramas_grid_test.dart | 2 +- test/discover/theater_pages_test.dart | 110 ++++++++++ 15 files changed, 588 insertions(+), 9 deletions(-) create mode 100644 lib/core/widgets/plain_page_scaffold.dart create mode 100644 lib/pages/new_releases/new_releases_page.dart create mode 100644 lib/pages/ranking/ranking_page.dart create mode 100644 lib/pages/reserve/reserve_page.dart create mode 100644 test/discover/theater_pages_test.dart diff --git a/lib/core/widgets/plain_page_scaffold.dart b/lib/core/widgets/plain_page_scaffold.dart new file mode 100644 index 0000000..640baf0 --- /dev/null +++ b/lib/core/widgets/plain_page_scaffold.dart @@ -0,0 +1,80 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../theme/app_colors.dart'; + +/// 二级页面通用骨架:返回栏 + 标题/副标题 + 可滚动内容,无底部导航。 +class PlainPageScaffold extends StatelessWidget { + const PlainPageScaffold({ + super.key, + required this.pageKey, + required this.backKey, + required this.title, + required this.child, + this.subtitle, + }); + + final Key pageKey; + final Key backKey; + final String title; + final String? subtitle; + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + key: pageKey, + backgroundColor: AppColors.ink, + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(6, 4, 14, 0), + child: Row( + children: [ + IconButton( + key: backKey, + tooltip: MaterialLocalizations.of( + context, + ).backButtonTooltip, + onPressed: () => Navigator.of(context).maybePop(), + icon: const Icon( + Icons.chevron_left_rounded, + color: AppColors.onSurface, + size: 26, + ), + ), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 20, + fontWeight: FontWeight.w800, + ), + ), + ), + ], + ), + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text( + subtitle!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: AppColors.muted, fontSize: 12), + ), + ), + Expanded(child: child), + ], + ), + ), + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee56f24..35d401b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -587,6 +587,16 @@ "rankingSubtitle": "Ranked from today's live catalog feed.", "rankingHot": "Trending", "rankingNew": "New releases", + "newReleasesSubtitle": "Newest arrivals", + "reserveSubtitle": "Reserve before launch", + "reserveEmptyTitle": "No reservations yet", + "reserveEmptyHint": "Reserve new dramas here once announced", + "topLabel": "TOP {n}", + "@topLabel": { + "placeholders": { + "n": {"type": "int"} + } + }, "rankingEmptyTitle": "No ranked stories yet", "rankingEmptyBody": "Titles appear here as soon as they are published.", "rankingTopPick": "TOP PICK" diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 4cba543..ffa2ba1 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -564,6 +564,11 @@ "rankingSubtitle": "Diurutkan dari feed katalog terkini.", "rankingHot": "Populer", "rankingNew": "Rilis baru", + "newReleasesSubtitle": "Rilis terbaru", + "reserveSubtitle": "Reservasi sebelum rilis", + "reserveEmptyTitle": "Belum ada reservasi", + "reserveEmptyHint": "Reservasi drama baru di sini", + "topLabel": "TOP {n}", "rankingEmptyTitle": "Belum ada peringkat", "rankingEmptyBody": "Judul muncul di sini setelah diterbitkan.", "rankingTopPick": "PILIHAN UTAMA" diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 776aa99..cfe633f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2518,6 +2518,36 @@ abstract class AppLocalizations { /// **'New releases'** String get rankingNew; + /// No description provided for @newReleasesSubtitle. + /// + /// In en, this message translates to: + /// **'Newest arrivals'** + String get newReleasesSubtitle; + + /// No description provided for @reserveSubtitle. + /// + /// In en, this message translates to: + /// **'Reserve before launch'** + String get reserveSubtitle; + + /// No description provided for @reserveEmptyTitle. + /// + /// In en, this message translates to: + /// **'No reservations yet'** + String get reserveEmptyTitle; + + /// No description provided for @reserveEmptyHint. + /// + /// In en, this message translates to: + /// **'Reserve new dramas here once announced'** + String get reserveEmptyHint; + + /// No description provided for @topLabel. + /// + /// In en, this message translates to: + /// **'TOP {n}'** + String topLabel(int n); + /// No description provided for @rankingEmptyTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 02eb36f..1c38a02 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1302,6 +1302,23 @@ class AppLocalizationsEn extends AppLocalizations { @override String get rankingNew => 'New releases'; + @override + String get newReleasesSubtitle => 'Newest arrivals'; + + @override + String get reserveSubtitle => 'Reserve before launch'; + + @override + String get reserveEmptyTitle => 'No reservations yet'; + + @override + String get reserveEmptyHint => 'Reserve new dramas here once announced'; + + @override + String topLabel(int n) { + return 'TOP $n'; + } + @override String get rankingEmptyTitle => 'No ranked stories yet'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 5be257a..0e12a5c 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -1302,6 +1302,23 @@ class AppLocalizationsId extends AppLocalizations { @override String get rankingNew => 'Rilis baru'; + @override + String get newReleasesSubtitle => 'Rilis terbaru'; + + @override + String get reserveSubtitle => 'Reservasi sebelum rilis'; + + @override + String get reserveEmptyTitle => 'Belum ada reservasi'; + + @override + String get reserveEmptyHint => 'Reservasi drama baru di sini'; + + @override + String topLabel(int n) { + return 'TOP $n'; + } + @override String get rankingEmptyTitle => 'Belum ada peringkat'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 10f6cae..074008a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1280,6 +1280,23 @@ class AppLocalizationsZh extends AppLocalizations { @override String get rankingNew => '新剧'; + @override + String get newReleasesSubtitle => '最新上架短剧'; + + @override + String get reserveSubtitle => '上线前预约,开播即通知'; + + @override + String get reserveEmptyTitle => '暂无预约剧集'; + + @override + String get reserveEmptyHint => '新剧上线后可在这里预约'; + + @override + String topLabel(int n) { + return 'TOP $n'; + } + @override String get rankingEmptyTitle => '暂无榜单内容'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 85c6b4b..e98add6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -526,6 +526,11 @@ "rankingSubtitle": "依据目录实时分区排序。", "rankingHot": "热播", "rankingNew": "新剧", + "newReleasesSubtitle": "最新上架短剧", + "reserveSubtitle": "上线前预约,开播即通知", + "reserveEmptyTitle": "暂无预约剧集", + "reserveEmptyHint": "新剧上线后可在这里预约", + "topLabel": "TOP {n}", "rankingEmptyTitle": "暂无榜单内容", "rankingEmptyBody": "新剧上线后会自动进入榜单。", "rankingTopPick": "今日榜首" diff --git a/lib/pages/discover/discover_view.dart b/lib/pages/discover/discover_view.dart index 9c90a4d..c5492a4 100644 --- a/lib/pages/discover/discover_view.dart +++ b/lib/pages/discover/discover_view.dart @@ -15,6 +15,9 @@ import '../../core/widgets/app_bottom_nav.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; import '../drama_detail/drama_detail_view.dart'; +import '../new_releases/new_releases_page.dart'; +import '../ranking/ranking_page.dart'; +import '../reserve/reserve_page.dart'; import 'discover_binding.dart'; import 'discover_logic.dart'; import 'discover_types.dart'; @@ -338,7 +341,7 @@ class _DiscoverPageState extends State { icon: Icons.local_fire_department_rounded, color: AppColors.blue, label: l10n.rankingTab, - onTap: () => _showComingSoon(l10n), + onTap: () => _openPage(context, const RankingPage()), ), ), const SizedBox(width: _toolbarGap), @@ -348,7 +351,7 @@ class _DiscoverPageState extends State { icon: Icons.play_circle_fill_rounded, color: _newIconColor, label: l10n.newDramas, - onTap: () => _showComingSoon(l10n), + onTap: () => _openPage(context, const NewReleasesPage()), ), ), const SizedBox(width: _toolbarGap), @@ -358,7 +361,7 @@ class _DiscoverPageState extends State { icon: Icons.calendar_month_rounded, color: AppColors.blue, label: l10n.reserve, - onTap: () => _showComingSoon(l10n), + onTap: () => _openPage(context, const ReservePage()), ), ), ], @@ -447,10 +450,8 @@ class _DiscoverPageState extends State { ); } - void _showComingSoon(AppLocalizations l10n) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.comingSoon))); + void _openPage(BuildContext context, Widget page) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); } Widget _block(Widget child) => diff --git a/lib/pages/discover/widgets/discover_search_field.dart b/lib/pages/discover/widgets/discover_search_field.dart index 4fd553c..f4a889d 100644 --- a/lib/pages/discover/widgets/discover_search_field.dart +++ b/lib/pages/discover/widgets/discover_search_field.dart @@ -74,7 +74,7 @@ class _DiscoverSearchFieldState extends State { height: _searchHeight, padding: const EdgeInsets.symmetric(horizontal: _searchPadding), decoration: BoxDecoration( - color: _focused ? AppColors.card : AppColors.commentInput, + color: AppColors.card, borderRadius: BorderRadius.circular(_searchRadius), border: Border.all( color: _focused ? AppColors.blue : Colors.transparent, diff --git a/lib/pages/new_releases/new_releases_page.dart b/lib/pages/new_releases/new_releases_page.dart new file mode 100644 index 0000000..d10e4a8 --- /dev/null +++ b/lib/pages/new_releases/new_releases_page.dart @@ -0,0 +1,43 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../core/data/services/catalog_controller.dart'; +import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/plain_page_scaffold.dart'; +import '../../l10n/app_localizations.dart'; +import '../discover/widgets/drama_grid.dart'; +import '../drama_detail/drama_detail_view.dart'; + +/// 剧场新剧页:按 id 倒序展示最新上架短剧。 +class NewReleasesPage extends StatelessWidget { + const NewReleasesPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final dramas = [...context.read().dramas] + ..sort((a, b) => b.id.compareTo(a.id)); + return PlainPageScaffold( + pageKey: const Key('new-releases-page'), + backKey: const Key('new-releases-back'), + title: l10n.rankingNew, + subtitle: l10n.newReleasesSubtitle, + child: dramas.isEmpty + ? EmptyState(icon: Icons.fiber_new_outlined, title: l10n.noStories) + : SingleChildScrollView( + padding: const EdgeInsets.only(top: 4, bottom: 24), + child: DramaGrid( + l10n: l10n, + dramas: dramas, + onOpen: (drama) => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DramaDetailPage(drama: drama), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/ranking/ranking_page.dart b/lib/pages/ranking/ranking_page.dart new file mode 100644 index 0000000..6f40663 --- /dev/null +++ b/lib/pages/ranking/ranking_page.dart @@ -0,0 +1,196 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../core/data/models/drama.dart'; +import '../../core/data/services/catalog_controller.dart'; +import '../../core/formatting/count_format.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_theme.dart'; +import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/plain_page_scaffold.dart'; +import '../../core/widgets/remote_image.dart'; +import '../../l10n/app_localizations.dart'; +import '../drama_detail/drama_detail_view.dart'; + +const int _topCount = 50; +const double _rankColumnWidth = 56; +const double _coverWidth = 72; +const double _coverHeight = 96; +const double _rowGap = 12; +const Color _rankSilver = Color(0xFFB8860B); +const Color _rankBronze = Color(0xFF8C837B); + +/// 剧场榜单页:按播放量降序展示 TOP 榜单。 +class RankingPage extends StatelessWidget { + const RankingPage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final dramas = [...context.read().dramas] + ..sort((a, b) => b.viewCount.compareTo(a.viewCount)); + final top = dramas.take(_topCount).toList(); + return PlainPageScaffold( + pageKey: const Key('ranking-page'), + backKey: const Key('ranking-back'), + title: l10n.topCharts, + subtitle: l10n.rankingSubtitle, + child: top.isEmpty + ? EmptyState(icon: Icons.leaderboard_outlined, title: l10n.noStories) + : ListView.separated( + padding: const EdgeInsets.fromLTRB(14, 4, 14, 24), + itemCount: top.length, + separatorBuilder: (_, _) => const SizedBox(height: _rowGap), + itemBuilder: (context, index) => + _RankingRow(index: index, drama: top[index], l10n: l10n), + ), + ); + } +} + +class _RankingRow extends StatelessWidget { + const _RankingRow({ + required this.index, + required this.drama, + required this.l10n, + }); + + final int index; + final Drama drama; + final AppLocalizations l10n; + + int get _rank => index + 1; + + Color get _rankColor => switch (_rank) { + 1 => AppColors.blue, + 2 => _rankSilver, + 3 => _rankBronze, + _ => AppColors.subtle, + }; + + double get _rankSize => _rank == 1 ? 22 : (_rank <= 3 ? 20 : 18); + + @override + Widget build(BuildContext context) { + final genre = drama.genre.isEmpty ? drama.category : drama.genre; + return Container( + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(14), + boxShadow: const [AppTheme.cardShadow], + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(14), + clipBehavior: Clip.antiAlias, + child: InkWell( + key: Key('ranking-row-$index'), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => DramaDetailPage(drama: drama)), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + SizedBox( + width: _rankColumnWidth, + child: Text( + l10n.topLabel(_rank), + key: Key('ranking-top-$_rank'), + textAlign: TextAlign.center, + maxLines: 1, + style: TextStyle( + color: _rankColor, + fontSize: _rankSize, + fontWeight: FontWeight.w900, + ), + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + width: _coverWidth, + height: _coverHeight, + child: RemoteImage( + drama.posterUrl, + fit: BoxFit.cover, + showFallbackIcon: true, + fallbackAspectRatio: 3 / 4, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + drama.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.onSurface, + fontSize: 15, + fontWeight: FontWeight.w700, + ), + ), + if (genre.isNotEmpty) ...[ + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: AppColors.raised, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + genre, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.muted, + fontSize: 11, + ), + ), + ), + ], + const SizedBox(height: 8), + Row( + children: [ + const Icon( + Icons.local_fire_department_rounded, + color: AppColors.blue, + size: 12, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + l10n.heatLabel( + formatInteractionCount(drama.viewCount), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.muted, + fontSize: 11, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/reserve/reserve_page.dart b/lib/pages/reserve/reserve_page.dart new file mode 100644 index 0000000..a680410 --- /dev/null +++ b/lib/pages/reserve/reserve_page.dart @@ -0,0 +1,48 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../core/theme/app_colors.dart'; +import '../../core/widgets/empty_state.dart'; +import '../../core/widgets/plain_page_scaffold.dart'; +import '../../l10n/app_localizations.dart'; + +/// 剧场预约页:暂无预约数据源,展示空态与即将上线说明。 +class ReservePage extends StatelessWidget { + const ReservePage({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return PlainPageScaffold( + pageKey: const Key('reserve-page'), + backKey: const Key('reserve-back'), + title: l10n.reserve, + subtitle: l10n.reserveSubtitle, + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + EmptyState( + key: const Key('reserve-empty'), + icon: Icons.event_available_outlined, + title: l10n.reserveEmptyTitle, + subtitle: l10n.reserveEmptyHint, + ), + const SizedBox(height: 12), + TextButton( + onPressed: null, + child: Text( + l10n.comingSoon, + style: const TextStyle(color: AppColors.subtle), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/test/discover/home_all_dramas_grid_test.dart b/test/discover/home_all_dramas_grid_test.dart index 982c8a6..19dc45a 100644 --- a/test/discover/home_all_dramas_grid_test.dart +++ b/test/discover/home_all_dramas_grid_test.dart @@ -183,7 +183,7 @@ void main() { as BoxDecoration; expect(shell().borderRadius, BorderRadius.circular(22)); - expect(shell().color, AppColors.commentInput); + expect(shell().color, AppColors.card); expect((shell().border! as Border).top.color, Colors.transparent); expect( tester.getSize(find.byKey(const Key('discover-search-shell'))).height, diff --git a/test/discover/theater_pages_test.dart b/test/discover/theater_pages_test.dart new file mode 100644 index 0000000..d18f091 --- /dev/null +++ b/test/discover/theater_pages_test.dart @@ -0,0 +1,110 @@ +// 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/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/pages/discover/widgets/drama_grid.dart'; +import 'package:fruitshorts/pages/drama_detail/drama_detail_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_catalog.dart'; +import '../support/fake_category.dart'; +import '../support/fake_haptics.dart'; +import '../support/fake_interaction_repositories.dart'; +import '../support/widget_player.dart'; + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); + + testWidgets('should_open_ranking_sorted_by_views', (tester) async { + await _pumpTheater(tester); + await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('ranking-page')), findsOneWidget); + expect(find.text('TOP 1'), findsOneWidget); + expect( + find.descendant( + of: find.byKey(const Key('ranking-row-0')), + matching: find.text('The Signal'), + ), + findsOneWidget, + ); + + await tester.tap(find.byKey(const Key('ranking-row-0'))); + await tester.pumpAndSettle(); + expect(find.byType(DramaDetailPage), findsOneWidget); + }); + + testWidgets('should_open_new_releases_sorted_by_id', (tester) async { + await _pumpTheater(tester); + await tester.tap(find.byKey(const Key('theater-toolbar-new'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('new-releases-page')), findsOneWidget); + final grid = tester.widget(find.byType(DramaGrid)); + expect(grid.dramas.map((drama) => drama.id).toList(), [ + 'vengeance-demo', + 'the-signal-demo', + 'city-lights-demo', + ]); + }); + + testWidgets('should_open_reserve_empty_state', (tester) async { + await _pumpTheater(tester); + await tester.tap(find.byKey(const Key('theater-toolbar-reserve'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('reserve-page')), findsOneWidget); + expect(find.byKey(const Key('reserve-empty')), findsOneWidget); + expect(find.text('Coming soon'), findsOneWidget); + expect( + tester.widget(find.byType(TextButton)).onPressed, + isNull, + ); + }); + + testWidgets('should_go_back_to_theater_from_ranking_page', (tester) async { + await _pumpTheater(tester); + await tester.tap(find.byKey(const Key('theater-toolbar-ranking'))); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('ranking-back'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('ranking-page')), findsNothing); + expect(find.byKey(const Key('discover-search')), findsOneWidget); + }); +} + +Future _pumpTheater(WidgetTester tester) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final catalog = FakeCatalogRepository(); + await tester.pumpWidget( + LumaDramaApp( + playerFactory: createWidgetPlayer, + catalogRepository: catalog, + searchRepository: catalog, + categoryRepository: FakeCategoryRepository(catalog: catalog), + watchHistoryRepository: InMemoryWatchHistoryRepository(), + favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), + playbackRepository: FakePlaybackRepository(), + commentRepository: FakeCommentRepository(), + interactionRepository: FakeInteractionRepository(), + haptics: FakeHaptics(), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('tab-theater'))); + await tester.pumpAndSettle(); +}