feat(app): 剧场页九宫格与极简我的页

This commit is contained in:
Zhou Weidong committed 2026-10-20 05:28:51 +08:00
1 parent dc8ca7b743
commit 35338cbfc8
46 files changed
+2353 -4358

No files matched your search

+38 -201
View File
@@ -1,173 +1,68 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/config/locale_controller.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/core/data/models/watch_progress.dart';
import 'package:fruitshorts/pages/mine/mine_logic.dart';
import 'package:provider/provider.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 'package:fruitshorts/pages/mine/mine_types.dart';
import '../support/fake_watch_history.dart';
Drama _drama(String id, {List<int> numbers = const [1, 2, 3]}) => Drama(
id: id,
title: id,
synopsis: 'synopsis',
posterUrl: '/files/poster.png',
episodes: [
for (final number in numbers)
Episode(
id: '$id-$number',
number: number,
title: 'Episode $number',
videoUrl: 'https://example.test/clip.mp4',
isFree: true,
),
],
const _record = WatchProgress(
dramaId: 'signal',
episodeId: 'signal-1',
position: Duration(seconds: 7),
duration: Duration(minutes: 2),
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'should_keep_requested_section_and_read_episode_one_when_constructed',
() async {
final history = FakeWatchHistory()..saved = const Duration(seconds: 7);
final logic = MineLogic(
history: history,
drama: _drama('signal'),
initialSection: 1,
);
test('should_load_recent_watches_when_constructed', () async {
final history = FakeWatchHistory()..recentRecords = const [_record];
final logic = MineLogic(history: history);
expect(logic.section, 1);
expect(logic.filter, 0);
expect(await logic.position, const Duration(seconds: 7));
expect(history.reads, ['signal']);
logic.dispose();
},
);
await pumpEventQueue();
test(
'should_resume_from_last_watch_when_list_card_has_no_episodes',
() async {
final history = FakeWatchHistory()
..saved = const Duration(seconds: 7)
..lastEpisode = 'signal-3';
final logic = MineLogic(
history: history,
drama: _drama('signal', numbers: const []),
initialSection: 1,
);
expect(await logic.position, const Duration(seconds: 7));
expect(history.reads, ['signal']);
logic.dispose();
},
);
test('should_notify_when_section_and_filter_are_selected', () {
final logic = MineLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
initialSection: 0,
);
var notifications = 0;
logic.addListener(() => notifications++);
logic.selectSection(1);
expect(logic.section, 1);
expect(notifications, 1);
logic.selectFilter(2);
expect(logic.filter, 2);
expect(notifications, 2);
expect(logic.recent.single.dramaId, 'signal');
expect(logic.recentLoading, isFalse);
expect(logic.recentError, isNull);
logic.dispose();
});
test('should_adopt_drama_only_when_page_is_active', () {
final history = FakeWatchHistory();
final logic = MineLogic(
history: history,
drama: _drama('signal'),
initialSection: 0,
);
test('should_expose_error_when_recent_request_fails', () async {
final history = FakeWatchHistory()..failReads = true;
final logic = MineLogic(history: history);
void sync(
Drama next, {
required bool active,
required bool wasActive,
required bool dramaChanged,
int sectionRequest = 0,
int previousSectionRequest = 0,
int initialSection = 0,
}) => logic.syncPage(
active: active,
wasActive: wasActive,
nextDrama: next,
dramaChanged: dramaChanged,
sectionRequest: sectionRequest,
previousSectionRequest: previousSectionRequest,
initialSection: initialSection,
);
await pumpEventQueue();
sync(_drama('city'), active: false, wasActive: true, dramaChanged: true);
expect(logic.drama.id, 'signal');
sync(_drama('city'), active: true, wasActive: false, dramaChanged: false);
expect(logic.drama.id, 'city');
history.reads.clear();
sync(
_drama('vengeance'),
active: true,
wasActive: true,
dramaChanged: true,
);
expect(logic.drama.id, 'vengeance');
expect(history.reads, ['vengeance']);
history.reads.clear();
sync(_drama('other'), active: true, wasActive: true, dramaChanged: false);
expect(logic.drama.id, 'vengeance');
expect(history.reads, isEmpty);
expect(logic.recent, isEmpty);
expect(logic.recentError, 'progressError');
logic.dispose();
});
test(
'should_reset_section_when_section_request_arrives_while_page_is_inactive',
() {
final logic = MineLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
initialSection: 0,
);
test('should_refresh_recent_only_when_page_becomes_active', () async {
final history = FakeWatchHistory()..recentRecords = const [_record];
final logic = MineLogic(history: history);
await pumpEventQueue();
logic.syncPage(
active: false,
wasActive: false,
nextDrama: logic.drama,
dramaChanged: false,
sectionRequest: 1,
previousSectionRequest: 0,
initialSection: 1,
);
history.recentRecords = const [];
logic.syncPage(active: true, wasActive: true);
await pumpEventQueue();
expect(logic.recent, hasLength(1));
expect(logic.section, 1);
logic.dispose();
},
);
logic.syncPage(active: true, wasActive: false);
await pumpEventQueue();
expect(logic.recent, isEmpty);
logic.dispose();
});
testWidgets('should_return_choice_and_forward_it_when_act_runs_embedded', (
testWidgets('should_forward_action_when_embedded_or_pop_when_standalone', (
tester,
) async {
final logic = MineLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
initialSection: 0,
);
final logic = MineLogic(history: FakeWatchHistory());
final forwarded = <MyPageAction>[];
final popped = Completer<MyPageAction?>();
late BuildContext pageContext;
@@ -191,7 +86,6 @@ void main() {
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
@@ -202,72 +96,15 @@ void main() {
onAction: forwarded.add,
);
expect(forwarded, [MyPageAction.discover]);
expect(Navigator.of(pageContext).canPop(), isTrue);
logic.act(pageContext, MyPageAction.resume, embedded: false);
await tester.pumpAndSettle();
await expectLater(popped.future, completion(MyPageAction.resume));
logic.dispose();
});
testWidgets('should_switch_app_locale_when_language_sheet_is_used', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
final locale = LocaleController();
final logic = MineLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
initialSection: 0,
);
late BuildContext context;
await tester.pumpWidget(
ChangeNotifierProvider<LocaleController>.value(
value: locale,
child: MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (inner) {
context = inner;
return const Scaffold(body: SizedBox.shrink());
},
),
),
),
);
unawaited(logic.showLanguage(context));
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(context)!;
expect(find.text(l10n.english), findsOneWidget);
expect(find.text(l10n.simplifiedChinese), findsOneWidget);
expect(find.text('Bahasa Indonesia'), findsOneWidget);
await tester.tap(find.text('Bahasa Indonesia'));
await tester.pumpAndSettle();
expect(locale.locale, const Locale('id'));
unawaited(logic.showLanguage(context));
await tester.pumpAndSettle();
await tester.tap(find.text(l10n.simplifiedChinese));
await tester.pumpAndSettle();
expect(locale.locale, const Locale('zh'));
logic.dispose();
});
test('should_do_nothing_when_notifying_after_dispose', () {
final logic = MineLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
initialSection: 0,
);
final logic = MineLogic(history: FakeWatchHistory());
var notifications = 0;
logic.addListener(() => notifications++);
+18 -56
View File
@@ -2,15 +2,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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 'package:fruitshorts/app.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/theme/app_colors.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';
@@ -20,7 +18,7 @@ import '../support/fake_notice_repository.dart';
import '../support/widget_player.dart';
void main() {
testWidgets('should_render_me_user_wallet_continue_and_language_rows', (
testWidgets('should_render_only_continue_and_six_core_entries', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
@@ -59,58 +57,22 @@ void main() {
await tester.pumpAndSettle();
expect(find.byKey(const Key('mine-user-row')), findsOneWidget);
expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
for (final key in const [Key('mine-search-button'), Key('mine-avatar')]) {
expect(tester.getSize(find.byKey(key)), const Size(36, 36));
expect(
tester
.widget<Icon>(
find.descendant(of: find.byKey(key), matching: find.byType(Icon)),
)
.size,
18,
);
}
expect(find.byKey(const Key('mine-continue-watching')), findsOneWidget);
expect(find.byKey(const Key('mine-language')), findsOneWidget);
expect(find.byKey(const Key('mine-library-tab-0')), findsOneWidget);
expect(find.byKey(const Key('mine-library-tab-1')), findsOneWidget);
expect(find.byKey(const Key('mine-library-tab-2')), findsOneWidget);
final tab0 = find.byKey(const Key('mine-library-tab-0'));
final tab1 = find.byKey(const Key('mine-library-tab-1'));
final tab2 = find.byKey(const Key('mine-library-tab-2'));
expect(tester.getTopLeft(tab0).dx, lessThan(tester.getTopLeft(tab1).dx));
expect(tester.getTopLeft(tab1).dx, lessThan(tester.getTopLeft(tab2).dx));
expect(
tester.getTopLeft(tab1).dx - tester.getTopRight(tab0).dx,
closeTo(20, 1),
);
expect(
(tester
.widget<AnimatedContainer>(
find.descendant(
of: tab0,
matching: find.byType(AnimatedContainer),
),
)
.decoration
as BoxDecoration)
.color,
AppColors.blue,
);
expect(
(tester
.widget<AnimatedContainer>(
find.descendant(
of: tab1,
matching: find.byType(AnimatedContainer),
),
)
.decoration
as BoxDecoration)
.color,
Colors.transparent,
);
expect(find.byKey(const Key('mine-continue-play')), findsOneWidget);
for (final key in const [
Key('mine-watchlist'),
Key('mine-history'),
Key('mine-downloads'),
Key('mine-wallet'),
Key('mine-vip'),
Key('mine-settings'),
]) {
expect(find.byKey(key), findsOneWidget);
}
expect(find.byKey(const Key('mine-language')), findsNothing);
expect(find.byKey(const Key('mine-library-tab-0')), findsNothing);
expect(find.byKey(const Key('mine-library-tab-1')), findsNothing);
expect(find.byKey(const Key('mine-library-tab-2')), findsNothing);
expect(tester.takeException(), isNull);
});
}
@@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.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 'package:fruitshorts/core/config/locale_controller.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/mine/mine_logic.dart';
const _drama = Drama(
id: 'drama-1',
title: 'The Signal',
synopsis: 'synopsis',
posterUrl: '/files/1.jpg',
);
void main() {
setUp(() {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
});
testWidgets('should_not_show_server_address_in_settings_sheet', (
tester,
) async {
await _pumpSettings(tester);
expect(find.byKey(const Key('server-endpoint-setting')), findsNothing);
expect(find.text('Server address'), findsNothing);
});
testWidgets('should_keep_all_three_languages_in_settings_sheet', (
tester,
) async {
await _pumpSettings(tester);
expect(find.text('English'), findsOneWidget);
expect(find.text('简体中文'), findsOneWidget);
expect(find.text('Bahasa Indonesia'), findsOneWidget);
});
}
Future<void> _pumpSettings(WidgetTester tester) async {
final logic = MineLogic(
history: InMemoryWatchHistoryRepository(),
drama: _drama,
initialSection: 0,
);
addTearDown(logic.dispose);
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => LocaleController(),
child: MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => Scaffold(
body: TextButton(
key: const Key('open-settings'),
onPressed: () => logic.showLanguage(context),
child: const Text('Open settings'),
),
),
),
),
),
);
await tester.tap(find.byKey(const Key('open-settings')));
await tester.pumpAndSettle();
}
+60
View File
@@ -0,0 +1,60 @@
// 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: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/fake_notice_repository.dart';
import '../support/widget_player.dart';
void main() {
setUp(() {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
});
testWidgets('should_open_minimal_settings_without_language_switches', (
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,
categoryRepository: FakeCategoryRepository(catalog: catalog),
watchHistoryRepository: InMemoryWatchHistoryRepository(),
favoriteRepository: InMemoryFavoriteRepository(),
playbackRepository: FakePlaybackRepository(),
commentRepository: FakeCommentRepository(),
interactionRepository: FakeInteractionRepository(),
noticeRepository: FakeNoticeRepository(),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('mine-settings')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('settings-page')), findsOneWidget);
expect(find.byKey(const Key('settings-notices')), findsOneWidget);
expect(find.byKey(const Key('server-endpoint-setting')), findsNothing);
expect(find.text('Server address'), findsNothing);
expect(find.text('English'), findsNothing);
expect(find.text('简体中文'), findsNothing);
expect(find.text('Bahasa Indonesia'), findsNothing);
});
}