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

+4 -1
View File
@@ -26,8 +26,11 @@ void main() {
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('mine-account-card')), findsNothing);
expect(find.byKey(const Key('settings-sign-in')), findsOneWidget);
});
testWidgets('should_show_four_account_entries_when_signed_in', (
@@ -46,7 +49,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -520));
await tester.tap(find.byKey(const Key('mine-settings')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('mine-account-card')), findsOneWidget);
+27 -44
View File
@@ -49,44 +49,34 @@ void main() {
},
);
test(
'should_map_category_id_to_discover_section_when_category_is_selected',
() async {
final repository = ScriptedCategoryRepository(
categories: const [
ContentCategory(id: '7', name: 'Romance'),
ContentCategory(id: '8', name: 'Action'),
test('should_return_category_dramas_when_category_id_exists', () async {
final repository = ScriptedCategoryRepository(
categories: const [
ContentCategory(id: '7', name: 'Romance'),
ContentCategory(id: '8', name: 'Action'),
],
feed: HomeFeed(
sections: [
FeedSection(code: 'hot', title: 'Hot', dramas: [_drama('hot-1')]),
FeedSection(code: 'new', title: 'New', dramas: [_drama('new-1')]),
FeedSection(
code: 'category-7',
title: 'Romance',
dramas: [_drama('romance-1')],
),
],
feed: HomeFeed(
sections: [
FeedSection(code: 'hot', title: 'Hot', dramas: [_drama('hot-1')]),
FeedSection(code: 'new', title: 'New', dramas: [_drama('new-1')]),
FeedSection(
code: 'category-7',
title: 'Romance',
dramas: [_drama('romance-1')],
),
],
),
);
final controller = CategoryController(repository);
),
);
final controller = CategoryController(repository);
await controller.load();
controller.selectCategory('7');
await controller.load();
expect(controller.selectedCode, 'category-7');
expect(controller.visibleDramas.map((drama) => drama.id), ['romance-1']);
expect(controller.selectedSectionMissing, isFalse);
controller.selectCategory('8');
expect(controller.selectedCode, 'category-8');
expect(controller.visibleDramas, isEmpty);
expect(controller.selectedSectionMissing, isTrue);
},
);
expect(controller.dramasOf('7').map((drama) => drama.id), ['romance-1']);
expect(controller.dramasOf('8'), isEmpty);
});
test(
'should_keep_hot_and_new_entries_when_discover_feed_provides_them',
'should_keep_hot_and_new_sections_when_discover_feed_provides_them',
() async {
final repository = ScriptedCategoryRepository(
feed: HomeFeed(
@@ -104,15 +94,12 @@ void main() {
final controller = CategoryController(repository);
await controller.load();
controller.selectCode(CategoryController.hotCode);
expect(controller.visibleDramas.map((drama) => drama.id), ['hot-1']);
controller.selectCode(CategoryController.newCode);
expect(controller.visibleDramas.map((drama) => drama.id), ['new-1']);
expect(controller.feed.sections.first.code, 'hot');
expect(controller.feed.sections[1].dramas.single.id, 'new-1');
},
);
test('should_dedupe_dramas_when_all_categories_are_selected', () async {
test('should_dedupe_dramas_when_feed_sections_overlap', () async {
final repository = ScriptedCategoryRepository(
feed: HomeFeed(
sections: [
@@ -129,11 +116,7 @@ void main() {
await controller.load();
expect(controller.selectedCode, CategoryController.allCode);
expect(controller.visibleDramas.map((drama) => drama.id), [
'same',
'other',
]);
expect(controller.feed.dramas.map((drama) => drama.id), ['same', 'other']);
});
test('should_clear_error_and_retry_when_initial_load_fails', () async {
+19
View File
@@ -0,0 +1,19 @@
// Author: fruit.com.cn
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/config/app_locale.dart';
void main() {
test('should_resolve_supported_device_languages', () {
expect(resolveAppLocale(const Locale('en')), const Locale('en'));
expect(resolveAppLocale(const Locale('zh')), const Locale('zh'));
expect(resolveAppLocale(const Locale('id')), const Locale('id'));
expect(resolveAppLocale(const Locale('in')), const Locale('id'));
});
test('should_fallback_to_indonesian_when_device_language_is_unsupported', () {
expect(resolveAppLocale(const Locale('fr')), const Locale('id'));
expect(resolveAppLocale(null), const Locale('id'));
});
}
-123
View File
@@ -1,123 +0,0 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/config/locale_controller.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 'package:shared_preferences_platform_interface/types.dart';
void main() {
setUp(() {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
});
test('should_keep_device_locale_when_no_choice_is_stored', () async {
final controller = LocaleController();
var notifications = 0;
controller.addListener(() => notifications++);
await controller.load();
expect(controller.locale, isNull);
expect(notifications, 1);
});
test('should_restore_chosen_language_when_app_restarts', () async {
final first = LocaleController();
await first.select('zh');
expect(first.locale, const Locale('zh'));
final reopened = LocaleController();
await reopened.load();
expect(reopened.locale, const Locale('zh'));
});
test('should_restore_indonesian_when_selected_and_app_restarts', () async {
final first = LocaleController();
await first.select('id');
expect(first.locale, const Locale('id'));
final reopened = LocaleController();
await reopened.load();
expect(reopened.locale, const Locale('id'));
});
test('should_fallback_to_indonesian_when_system_language_is_unsupported', () {
expect(resolveAppLocale(null, const Locale('en')), const Locale('en'));
expect(resolveAppLocale(null, const Locale('id')), const Locale('id'));
expect(resolveAppLocale(null, const Locale('fr')), const Locale('id'));
expect(resolveAppLocale(null, const Locale('in')), const Locale('id'));
expect(
resolveAppLocale(const Locale('zh'), const Locale('fr')),
const Locale('zh'),
);
});
test(
'should_clear_stored_chinese_preference_when_english_is_selected',
() async {
await LocaleController().select('zh');
final reopened = LocaleController();
await reopened.load();
await reopened.select('en');
final third = LocaleController();
await third.load();
expect(third.locale, const Locale('en'));
},
);
test('should_clear_locale_when_code_is_unsupported', () async {
final controller = LocaleController();
await controller.select('zh');
expect(controller.locale, const Locale('zh'));
await controller.select('tlh');
expect(controller.locale, isNull);
});
test(
'should_resolve_to_device_locale_when_stored_code_is_unsupported',
() async {
await SharedPreferencesAsync().setString('ui.locale', 'fr');
final controller = LocaleController();
await controller.load();
expect(controller.locale, isNull);
},
);
test(
'should_survive_load_and_select_when_preference_store_is_broken',
() async {
SharedPreferencesAsyncPlatform.instance = _BrokenPreferences();
final controller = LocaleController();
await controller.load();
expect(controller.locale, isNull);
await controller.select('zh');
expect(controller.locale, const Locale('zh'));
},
);
}
/// Stands in for a platform whose preference store is unavailable.
base class _BrokenPreferences extends InMemorySharedPreferencesAsync {
_BrokenPreferences() : super.empty();
@override
Future<String?> getString(String key, SharedPreferencesOptions options) =>
Future<String?>.error(StateError('preferences unavailable'));
@override
Future<bool> setString(
String key,
String value,
SharedPreferencesOptions options,
) => Future<bool>.error(StateError('preferences unavailable'));
}
+31 -323
View File
@@ -1,255 +1,32 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/discover/discover_logic.dart';
import '../support/fake_watch_history.dart';
Drama _drama(
String id, {
List<int> numbers = const [1, 2, 3],
String language = '',
}) => Drama(
language: language,
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,
),
],
);
import 'package:fruitshorts/pages/discover/discover_types.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'should_resolve_resume_position_of_episode_one_when_constructed',
() async {
final history = FakeWatchHistory()..saved = const Duration(seconds: 4);
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
test('should_report_query_only_for_non_whitespace_text', () {
final logic = DiscoverLogic();
expect(await logic.position, const Duration(seconds: 4));
expect(history.reads, ['signal']);
expect(logic.searchOpen, isFalse);
logic.dispose();
},
);
expect(logic.hasSearchQuery, isFalse);
test('should_reread_resume_position_when_drama_is_swapped', () async {
final history = FakeWatchHistory();
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
logic.search.text = ' ';
expect(logic.hasSearchQuery, isFalse);
logic.drama = _drama('city');
logic.refreshPosition();
await logic.position;
expect(history.reads, ['signal', 'city']);
logic.search.text = 'remote';
expect(logic.hasSearchQuery, isTrue);
logic.dispose();
});
test(
'should_resume_from_last_watch_when_list_card_has_no_episodes',
() async {
final history = FakeWatchHistory()
..saved = const Duration(seconds: 9)
..lastEpisode = 'signal-2';
final logic = DiscoverLogic(
history: history,
drama: _drama('signal', numbers: const []),
);
expect(await logic.position, const Duration(seconds: 9));
expect(history.reads, ['signal']);
logic.dispose();
},
);
test('should_filter_visible_dramas_when_language_is_selected', () {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final dramas = [
_drama('zh', language: 'zh-CN'),
_drama('id', language: 'id'),
_drama('unknown'),
];
expect(logic.visibleDramas(dramas), hasLength(3));
logic.selectLanguage('id');
expect(logic.visibleDramas(dramas).map((item) => item.id), ['id']);
logic.selectLanguage(null);
expect(logic.visibleDramas(dramas), hasLength(3));
logic.dispose();
});
test('should_list_distinct_languages_when_feed_has_cards', () {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final dramas = [
_drama('zh', language: 'zh-CN'),
_drama('id', language: 'id'),
_drama('id2', language: 'id'),
_drama('unknown'),
];
expect(logic.languagesOf(dramas), ['zh-CN', 'id']);
logic.dispose();
});
test('should_have_no_resume_position_when_server_has_no_record', () async {
final history = FakeWatchHistory();
final logic = DiscoverLogic(
history: history,
drama: _drama('signal', numbers: const []),
);
expect(await logic.position, isNull);
// 续看位置改为按剧问服务端:即使卡片没有剧集也会读一次
expect(history.reads, ['signal']);
logic.dispose();
});
test('should_show_search_when_flag_is_on_or_text_is_typed', () {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
expect(logic.showSearch, isFalse);
logic.search.text = 'vengeance';
logic.onSearchChanged();
expect(logic.showSearch, isTrue);
logic.search.clear();
expect(logic.showSearch, isFalse);
logic.searchOpen = true;
expect(logic.showSearch, isTrue);
logic.dispose();
});
test('should_match_by_id_and_tolerate_missing_id_when_looking_up_drama', () {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final catalog = [_drama('signal'), _drama('city')];
expect(logic.dramaFor('city', catalog)?.id, 'city');
expect(logic.dramaFor('missing', catalog), isNull);
expect(logic.dramaFor(null, catalog), isNull);
expect(logic.dramaFor('city', const []), isNull);
logic.dispose();
});
test('should_adopt_drama_only_when_page_is_active', () {
final history = FakeWatchHistory();
final logic = DiscoverLogic(history: history, drama: _drama('signal'));
void sync(
Drama next, {
required bool active,
required bool wasActive,
required bool dramaChanged,
int searchRequest = 0,
int previousSearchRequest = 0,
}) => logic.syncDrama(
active: active,
wasActive: wasActive,
nextDrama: next,
dramaChanged: dramaChanged,
searchRequest: searchRequest,
previousSearchRequest: previousSearchRequest,
isActive: () => active,
);
// An inactive page keeps showing its own drama.
sync(_drama('city'), active: false, wasActive: true, dramaChanged: true);
expect(logic.drama.id, 'signal');
// Becoming active adopts the incoming drama.
sync(_drama('city'), active: true, wasActive: false, dramaChanged: false);
expect(logic.drama.id, 'city');
// Staying active with a changed drama re-reads history.
history.reads.clear();
sync(
_drama('vengeance'),
active: true,
wasActive: true,
dramaChanged: true,
);
expect(logic.drama.id, 'vengeance');
expect(history.reads, ['vengeance']);
// Staying active with no change leaves both drama and history alone.
history.reads.clear();
sync(_drama('other'), active: true, wasActive: true, dramaChanged: false);
expect(logic.drama.id, 'vengeance');
expect(history.reads, isEmpty);
logic.dispose();
});
testWidgets(
'should_open_and_focus_field_when_search_is_requested_and_page_is_active',
(tester) async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TextField(
controller: logic.search,
focusNode: logic.searchFocus,
),
),
),
);
logic.syncDrama(
active: true,
wasActive: true,
nextDrama: logic.drama,
dramaChanged: false,
searchRequest: 1,
previousSearchRequest: 0,
isActive: () => true,
);
expect(logic.searchOpen, isTrue);
await tester.pumpAndSettle();
expect(logic.searchFocus.hasFocus, isTrue);
await tester.pumpWidget(const SizedBox.shrink());
logic.dispose();
},
);
testWidgets('should_keep_search_field_closed_when_page_is_hidden', (
testWidgets('should_focus_search_only_for_new_request_on_active_page', (
tester,
) async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final logic = DiscoverLogic();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@@ -261,33 +38,33 @@ void main() {
),
);
// The search request branch is gated on `active`, so a page that is not on
// screen must neither reveal the field nor pull focus away from the visible tab.
logic.syncDrama(
logic.syncSearch(
active: false,
wasActive: false,
nextDrama: logic.drama,
dramaChanged: false,
searchRequest: 1,
previousSearchRequest: 0,
request: 1,
previousRequest: 0,
isActive: () => false,
);
expect(logic.searchOpen, isFalse);
await tester.pumpAndSettle();
await tester.pump();
expect(logic.searchFocus.hasFocus, isFalse);
logic.syncSearch(
active: true,
request: 2,
previousRequest: 1,
isActive: () => true,
);
await tester.pump();
await tester.pump();
expect(logic.searchFocus.hasFocus, isTrue);
await tester.pumpWidget(const SizedBox.shrink());
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 = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final logic = DiscoverLogic();
final forwarded = <DiscoverPageAction>[];
final popped = Completer<DiscoverPageAction?>();
late BuildContext pageContext;
@@ -311,7 +88,6 @@ void main() {
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
@@ -322,83 +98,15 @@ void main() {
onAction: forwarded.add,
);
expect(forwarded, [DiscoverPageAction.home]);
expect(Navigator.of(pageContext).canPop(), isTrue);
logic.act(pageContext, DiscoverPageAction.watchlist, embedded: false);
logic.act(pageContext, DiscoverPageAction.my, embedded: false);
await tester.pumpAndSettle();
await expectLater(popped.future, completion(DiscoverPageAction.watchlist));
logic.dispose();
});
testWidgets('should_show_artwork_notice_when_card_has_no_playable_media', (
tester,
) async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
late BuildContext context;
await tester.pumpWidget(
MaterialApp(
locale: const Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (inner) {
context = inner;
return const Scaffold(body: SizedBox.shrink());
},
),
),
);
logic.openCard(context, null, embedded: false);
await tester.pumpAndSettle();
final l10n = AppLocalizations.of(context)!;
expect(find.text(l10n.artworkOnlyNotice), findsOneWidget);
logic.dispose();
});
testWidgets('should_forward_chosen_drama_when_card_is_embedded', (
tester,
) async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
late BuildContext context;
Drama? selected;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (inner) {
context = inner;
return const SizedBox.shrink();
},
),
),
);
logic.openCard(
context,
_drama('city'),
embedded: true,
onDramaSelected: (drama) => selected = drama,
);
expect(selected?.id, 'city');
await expectLater(popped.future, completion(DiscoverPageAction.my));
logic.dispose();
});
test('should_do_nothing_when_notifying_after_dispose', () {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('s'),
);
final logic = DiscoverLogic();
var notifications = 0;
logic.addListener(() => notifications++);
+10 -22
View File
@@ -7,8 +7,6 @@ import 'package:fruitshorts/core/data/remote/api_exception.dart';
import 'package:fruitshorts/core/data/repositories/search_repository.dart';
import 'package:fruitshorts/pages/discover/discover_logic.dart';
import '../support/fake_watch_history.dart';
Drama _drama(String id, {String language = 'id'}) => Drama(
id: id,
title: id,
@@ -25,11 +23,8 @@ void main() {
'should_query_backend_and_render_results_when_search_text_changes',
() async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
final logic = DiscoverLogic(searchRepository: repository)
..searchDebounce = Duration.zero;
logic.search.text = 'velvet';
logic.onSearchChanged();
@@ -39,18 +34,15 @@ void main() {
(keyword: 'velvet', pageIndex: 1, pageSize: 20),
]);
expect(logic.searchLoading, isFalse);
expect(logic.visibleDramas(const []).single.id, 'velvet');
expect(logic.searchResults.single.id, 'velvet');
logic.dispose();
},
);
test('should_keep_server_results_when_text_only_loses_whitespace', () async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
final logic = DiscoverLogic(searchRepository: repository)
..searchDebounce = Duration.zero;
logic.search.text = 'velvet ';
logic.onSearchChanged();
@@ -60,14 +52,12 @@ void main() {
await pumpEventQueue();
expect(repository.calls, hasLength(1));
expect(logic.visibleDramas(const []).single.id, 'velvet');
expect(logic.searchResults.single.id, 'velvet');
logic.dispose();
});
test('should_expose_error_message_when_search_fails', () async {
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: _FakeSearchRepository(
error: const ApiException(400, 'Keyword is invalid'),
),
@@ -84,11 +74,8 @@ void main() {
test('should_clear_server_results_when_search_text_is_empty', () async {
final repository = _FakeSearchRepository(results: [_drama('velvet')]);
final logic = DiscoverLogic(
history: FakeWatchHistory(),
drama: _drama('signal'),
searchRepository: repository,
)..searchDebounce = Duration.zero;
final logic = DiscoverLogic(searchRepository: repository)
..searchDebounce = Duration.zero;
logic.search.text = 'velvet';
logic.onSearchChanged();
@@ -96,7 +83,8 @@ void main() {
logic.search.clear();
logic.onSearchChanged();
expect(logic.visibleDramas([_drama('local')]).single.id, 'local');
expect(logic.hasSearchQuery, isFalse);
expect(logic.searchResults, isEmpty);
logic.dispose();
});
}
+62 -327
View File
@@ -1,17 +1,17 @@
// Author: fruit.com.cn
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/content_category.dart';
import 'package:fruitshorts/core/data/models/drama.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/category_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: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_interaction_repositories.dart';
@@ -24,173 +24,65 @@ void main() {
InMemorySharedPreferencesAsync.empty();
});
testWidgets(
'should_show_all_dramas_in_two_column_masonry_without_continue_watching',
(tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
testWidgets('should_show_search_categories_ranking_and_all_dramas', (
tester,
) async {
await _pumpTheater(tester, categoryRepository: _GridCategoryRepository());
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.text('Continue Watching'), findsNothing);
expect(find.byKey(const Key('discover-search')), findsOneWidget);
expect(find.byKey(const Key('discover-category-grid')), findsOneWidget);
expect(find.byKey(const Key('discover-ranking-section')), findsOneWidget);
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect(find.byKey(const Key('ranking-tab-hot')), findsOneWidget);
expect(find.byKey(const Key('ranking-tab-new')), findsOneWidget);
expect(find.text('Continue Watching'), findsNothing);
expect(find.text('Recommended'), findsOneWidget);
for (final key in const [
Key('discover-search-button'),
Key('discover-profile'),
]) {
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.text('For You'), findsNothing);
final masonry = tester.widget<Row>(
find.byKey(const Key('home-all-dramas-grid')),
);
expect(masonry.children, hasLength(3));
expect((masonry.children[1] as SizedBox).width, 10);
expect(
tester
.widget<Padding>(find.byKey(const Key('home-all-dramas-section')))
.padding,
const EdgeInsets.symmetric(horizontal: 14),
);
expect(find.byType(GridView), findsNothing);
expect(find.byType(CustomScrollView), findsOneWidget);
final signalCard = find.byKey(
const Key('home-drama-card-the-signal-demo'),
);
final cityCard = find.byKey(
const Key('home-drama-card-city-lights-demo'),
);
final vengeanceCard = find.byKey(
const Key('home-drama-card-vengeance-demo'),
);
expect(tester.getTopLeft(signalCard).dy, tester.getTopLeft(cityCard).dy);
expect(
tester.getTopLeft(vengeanceCard).dx,
tester.getTopLeft(signalCard).dx,
);
expect(
tester.getTopLeft(vengeanceCard).dy -
tester.getBottomLeft(signalCard).dy,
closeTo(10, 1),
);
final cover = find.byKey(const Key('home-drama-cover-city-lights-demo'));
expect(tester.getSize(cover).height, greaterThan(120));
expect(
tester
.widget<Image>(
find.descendant(of: cover, matching: find.byType(Image)),
)
.fit,
BoxFit.fitWidth,
);
expect(
find.descendant(
of: cover,
matching: find.text('City Lights & Velvet Nights'),
),
findsNothing,
);
expect(
find.byKey(const Key('home-drama-title-city-lights-demo')),
findsOneWidget,
);
expect(
tester
.getTopLeft(
find.byKey(const Key('home-drama-title-city-lights-demo')),
)
.dy,
greaterThan(tester.getBottomLeft(cover).dy),
);
expect(
tester
.widget<Text>(
find.byKey(const Key('home-drama-title-city-lights-demo')),
)
.maxLines,
1,
);
expect(
tester
.widget<Text>(
find.byKey(const Key('home-drama-genre-city-lights-demo')),
)
.maxLines,
1,
);
expect(tester.takeException(), isNull);
},
);
testWidgets('should_update_grid_when_category_is_selected', (tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.text('The Signal'), findsWidgets);
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
await tester.tap(find.text('Romance'));
await tester.pumpAndSettle();
expect(find.text('The Signal'), findsNothing);
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsNothing,
final grid = tester.widget<GridView>(
find.byKey(const Key('discover-category-grid')),
);
final delegate =
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
expect(delegate.crossAxisCount, 3);
expect(tester.takeException(), isNull);
});
testWidgets('should_open_category_dramas_page_when_tile_is_tapped', (
tester,
) async {
await _pumpTheater(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('discover-category-7')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('category-dramas-page')), findsOneWidget);
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsNothing,
);
});
testWidgets('should_keep_discover_header_fixed_when_content_scrolls', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(
await _pumpTheater(
tester,
categoryRepository: _GridCategoryRepository(
discoverFeed: _tallDiscoverFeed,
),
);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
final headerBefore = tester.getTopLeft(
find.byKey(const Key('discover-search-button')),
);
final contentBefore = tester.getTopLeft(
find.byKey(const Key('home-category-chips')),
find.byKey(const Key('discover-category-grid')),
);
await tester.drag(find.byType(CustomScrollView), const Offset(0, -40));
await tester.drag(find.byType(ListView), const Offset(0, -140));
await tester.pumpAndSettle();
expect(
@@ -198,158 +90,37 @@ void main() {
headerBefore,
);
expect(
tester.getTopLeft(find.byKey(const Key('home-category-chips'))).dy,
tester.getTopLeft(find.byKey(const Key('discover-category-grid'))).dy,
lessThan(contentBefore.dy),
);
});
testWidgets('should_keep_empty_messages_when_feed_has_no_dramas', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(
testWidgets('should_show_empty_and_retry_states', (tester) async {
await _pumpTheater(
tester,
categoryRepository: _GridCategoryRepository(
discoverFeed: const HomeFeed(),
),
);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('discover-empty')), findsOneWidget);
expect(find.text('No matching dramas.'), findsOneWidget);
expect(find.byKey(const Key('empty-state-content')), findsOneWidget);
await tester.pumpWidget(const SizedBox.shrink());
await _pumpTheater(
tester,
categoryRepository: _FailingCategoryRepository(),
);
await tester.tap(find.text('Romance'));
await tester.pumpAndSettle();
expect(find.text('No dramas in this category yet.'), findsOneWidget);
});
testWidgets('should_show_retry_when_category_feed_fails', (tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(tester, categoryRepository: _FailingCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.text('Categories could not be loaded.'), findsOneWidget);
expect(find.byKey(const Key('home-categories-error')), findsOneWidget);
expect(find.text('Retry'), findsOneWidget);
expect(find.byKey(const Key('empty-state-icon')), findsOneWidget);
final icon = tester.getCenter(find.byKey(const Key('empty-state-icon')));
final feed = tester.getRect(find.byType(CustomScrollView));
expect(icon.dx, closeTo(feed.center.dx, 1));
expect(find.byKey(const Key('home-all-dramas-grid')), findsNothing);
});
testWidgets('should_fit_two_column_grid_on_compact_screen', (tester) async {
tester.view.physicalSize = const Size(320, 568);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(
find.byKey(const Key('home-drama-card-city-lights-demo')),
180,
scrollable: find.byType(Scrollable).last,
);
await tester.pumpAndSettle();
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
expect(tester.takeException(), isNull);
});
testWidgets('should_filter_trending_grid_when_search_text_changes', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
);
testWidgets('should_show_search_results_when_text_changes', (tester) async {
await _pumpTheater(tester, categoryRepository: _GridCategoryRepository());
await tester.enterText(find.byKey(const Key('discover-search')), 'velvet');
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsNothing,
);
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
);
await tester.enterText(find.byKey(const Key('discover-search')), '');
await tester.pumpAndSettle();
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsOneWidget,
);
});
testWidgets('should_unfocus_search_without_losing_input_or_filter', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(tester, categoryRepository: _GridCategoryRepository());
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('discover-search')));
await tester.pump();
expect(
tester
.widget<TextField>(find.byKey(const Key('discover-search')))
.focusNode!
.hasFocus,
isTrue,
);
await tester.enterText(find.byKey(const Key('discover-search')), 'velvet');
await tester.pumpAndSettle();
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
);
expect(
find.byKey(const Key('home-drama-card-the-signal-demo')),
findsNothing,
);
await tester.tapAt(const Offset(70, 23));
await tester.pump();
final field = tester.widget<TextField>(
find.byKey(const Key('discover-search')),
);
expect(field.focusNode!.hasFocus, isFalse);
expect(field.controller!.text, 'velvet');
expect(
find.byKey(const Key('home-drama-card-city-lights-demo')),
findsOneWidget,
@@ -361,20 +132,13 @@ void main() {
});
testWidgets('should_refresh_feed_when_pulled_down', (tester) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final counter = _CountingCategoryRepository(
_GridCategoryRepository(discoverFeed: _tallDiscoverFeed),
);
await _pumpApp(tester, categoryRepository: counter);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
await _pumpTheater(tester, categoryRepository: counter);
final before = counter.categoryLoads + counter.feedLoads;
await tester.drag(find.byType(CustomScrollView), const Offset(0, 320));
await tester.drag(find.byType(ListView), const Offset(0, 320));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
@@ -382,47 +146,16 @@ void main() {
expect(counter.categoryLoads + counter.feedLoads, greaterThan(before));
expect(find.byType(RefreshProgressIndicator), findsNothing);
});
testWidgets('should_show_continue_watching_only_when_a_record_exists', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await _pumpApp(
tester,
categoryRepository: _GridCategoryRepository(),
history: InMemoryWatchHistoryRepository(
seed: const [
WatchProgress(
dramaId: 'city-lights-demo',
episodeId: 'city-lights-demo-1',
position: Duration(seconds: 42),
duration: Duration(minutes: 2),
),
],
),
);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('discover-continue-watching')), findsOneWidget);
expect(find.byKey(const Key('discover-continue-play')), findsOneWidget);
expect(find.text('City Lights & Velvet Nights'), findsWidgets);
expect(find.byKey(const Key('discover-quick-filter')), findsOneWidget);
expect(find.byKey(const Key('discover-quick-charts')), findsOneWidget);
expect(find.byKey(const Key('discover-quick-new')), findsOneWidget);
expect(find.byKey(const Key('discover-quick-watchlist')), findsOneWidget);
});
}
Future<void> _pumpApp(
Future<void> _pumpTheater(
WidgetTester tester, {
required CategoryRepository categoryRepository,
WatchHistoryRepository? history,
}) 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(
@@ -430,7 +163,7 @@ Future<void> _pumpApp(
catalogRepository: catalog,
searchRepository: catalog,
categoryRepository: categoryRepository,
watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(),
watchHistoryRepository: InMemoryWatchHistoryRepository(),
favoriteRepository: InMemoryFavoriteRepository(),
playbackRepository: FakePlaybackRepository(),
commentRepository: FakeCommentRepository(),
@@ -439,6 +172,8 @@ Future<void> _pumpApp(
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
}
class _GridCategoryRepository implements CategoryRepository {
+27 -94
View File
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/pages/discover/discover_logic.dart';
import 'package:fruitshorts/pages/discover/discover_types.dart';
import 'package:fruitshorts/pages/home/home_logic.dart';
import 'package:fruitshorts/pages/mine/mine_logic.dart';
import 'package:fruitshorts/pages/mine/mine_types.dart';
Drama _drama(String id) => Drama(
id: id,
@@ -18,20 +19,15 @@ void main() {
final dramas = [_drama('signal'), _drama('city')];
test(
'should_make_first_catalog_entry_the_current_drama_when_catalog_loads',
() {
final logic = HomeLogic(dramas);
test('should_make_first_catalog_entry_current_when_catalog_loads', () {
final logic = HomeLogic(dramas);
expect(logic.currentDrama.id, 'signal');
expect(logic.tab, 0);
expect(logic.mySection, 0);
expect(logic.drawerOpen, isFalse);
expect(logic.profile, isFalse);
expect(logic.videoImmersive, isFalse);
logic.dispose();
},
);
expect(logic.currentDrama.id, 'signal');
expect(logic.tab, 0);
expect(logic.drawerOpen, isFalse);
expect(logic.videoImmersive, isFalse);
logic.dispose();
});
test('should_not_build_shell_when_catalog_is_empty', () {
expect(() => HomeLogic(const []), throwsStateError);
@@ -46,7 +42,6 @@ void main() {
expect(logic.tab, 1);
expect(logic.searchRequest, 0);
expect(logic.sectionRequest, 0);
expect(logic.resumeRequest, 0);
expect(logic.backRequest, 0);
expect(notifications, 1);
@@ -57,62 +52,27 @@ void main() {
final logic = HomeLogic(dramas);
logic.select(1, search: true);
expect(logic.tab, 1);
expect(logic.searchRequest, 1);
expect(logic.sectionRequest, 0);
logic.select(2, mySection: 0);
expect(logic.mySection, 0);
expect(logic.sectionRequest, 1);
expect(logic.searchRequest, 1);
expect(logic.resumeRequest, 0);
logic.select(0, resume: true);
expect(logic.resumeRequest, 1);
expect(logic.sectionRequest, 1);
expect(logic.backRequest, 0);
expect(logic.searchRequest, 1);
logic.dispose();
});
test('should_default_to_second_section_when_watchlist_shortcut_is_used', () {
test('should_open_profile_tab_when_profile_action_is_used', () {
final logic = HomeLogic(dramas);
logic.select(2, watchlist: true);
logic.openProfile();
expect(logic.tab, 2);
expect(logic.mySection, 1);
expect(logic.sectionRequest, 1);
logic.dispose();
});
test(
'should_differ_on_profile_flag_when_open_profile_and_open_library_are_used',
() {
final logic = HomeLogic(dramas);
logic.openProfile();
expect(logic.profile, isTrue);
expect(logic.tab, 2);
expect(logic.mySection, 0);
expect(logic.sectionRequest, 1);
logic.openLibrary(section: 1);
expect(logic.profile, isFalse);
expect(logic.mySection, 1);
expect(logic.sectionRequest, 2);
logic.openLibrary();
expect(logic.profile, isFalse);
expect(logic.mySection, 1);
expect(logic.sectionRequest, 3);
logic.dispose();
},
);
test('should_return_to_discover_and_handle_immersive_back', () {
final logic = HomeLogic(dramas);
// Home is the launch tab; the first back press lands on Discover.
logic.onSystemBack();
expect(logic.tab, 1);
logic.onSystemBack();
@@ -122,7 +82,6 @@ void main() {
logic.select(0);
logic.onSystemBack();
expect(logic.tab, 1);
expect(logic.backRequest, 0);
logic.select(0);
logic.onImmersiveChanged(true);
@@ -181,6 +140,17 @@ void main() {
logic.dispose();
});
test('should_map_onto_root_tabs_when_discover_actions_fire', () {
final logic = HomeLogic(dramas);
logic.discoverAction(DiscoverPageAction.my);
expect(logic.tab, 2);
logic.discoverAction(DiscoverPageAction.home);
expect(logic.tab, 0);
logic.dispose();
});
test('should_do_nothing_when_notifying_after_dispose', () {
final logic = HomeLogic(dramas);
var notifications = 0;
@@ -191,41 +161,4 @@ void main() {
expect(notifications, 0);
});
testWidgets('should_map_onto_root_tabs_when_discover_actions_fire', (
tester,
) async {
final logic = HomeLogic(dramas);
late BuildContext context;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (inner) {
context = inner;
return const SizedBox.shrink();
},
),
),
);
logic.discoverAction(context, DiscoverPageAction.watchlist);
expect(logic.tab, 2);
expect(logic.profile, isFalse);
expect(logic.mySection, 1);
expect(logic.sectionRequest, 1);
logic.discoverAction(context, DiscoverPageAction.my);
expect(logic.tab, 2);
expect(logic.profile, isFalse);
logic.discoverAction(context, DiscoverPageAction.resume);
expect(logic.tab, 0);
expect(logic.resumeRequest, 1);
logic.discoverAction(context, DiscoverPageAction.home);
expect(logic.tab, 0);
expect(tester.takeException(), isNull);
logic.dispose();
});
}
+7 -10
View File
@@ -95,9 +95,7 @@ void main() {
);
});
testWidgets('should_map_three_tabs_to_home_discover_and_me', (
tester,
) async {
testWidgets('should_map_three_tabs_to_home_discover_and_me', (tester) async {
await _pumpApp(tester);
expect(find.byKey(const Key('tab-home')), findsOneWidget);
@@ -112,10 +110,9 @@ void main() {
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
expect(find.text('Viewer'), findsOneWidget);
expect(
find.byKey(const Key('mine-library-tab-0'), skipOffstage: false),
findsOneWidget,
);
expect(find.byKey(const Key('mine-menu')), findsOneWidget);
expect(find.byKey(const Key('mine-watchlist')), findsOneWidget);
expect(find.byKey(const Key('mine-library-tab-0')), findsNothing);
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
@@ -131,11 +128,11 @@ void main() {
await tester.pumpAndSettle();
haptics.reset();
await tester.drag(find.byType(ListView), const Offset(0, -700));
await tester.pumpAndSettle();
await tester.tap(find.text('App Language'));
await tester.ensureVisible(find.byKey(const Key('mine-settings')));
await tester.tap(find.byKey(const Key('mine-settings')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('settings-page')), findsOneWidget);
expect(haptics.calls, isEmpty);
});
}
+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();
}
@@ -2,19 +2,15 @@
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/content_category.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/core/data/repositories/category_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: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';
@@ -25,19 +21,20 @@ void main() {
InMemorySharedPreferencesAsync.empty();
});
testWidgets('should_filter_theater_grid_when_language_chip_is_tapped', (
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: FakeCatalogRepository(),
categoryRepository: _MixedLanguageCategoryRepository(),
catalogRepository: catalog,
categoryRepository: FakeCategoryRepository(catalog: catalog),
watchHistoryRepository: InMemoryWatchHistoryRepository(),
favoriteRepository: InMemoryFavoriteRepository(),
playbackRepository: FakePlaybackRepository(),
@@ -47,44 +44,17 @@ void main() {
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-discover')));
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('home-drama-card-zh-demo')), findsOneWidget);
expect(find.byKey(const Key('home-drama-card-id-demo')), findsOneWidget);
await tester.tap(find.byKey(const Key('discover-language-zh-CN')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('home-drama-card-zh-demo')), findsOneWidget);
expect(find.byKey(const Key('home-drama-card-id-demo')), findsNothing);
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);
});
}
class _MixedLanguageCategoryRepository implements CategoryRepository {
@override
Future<List<ContentCategory>> fetchCategories() async => const [];
@override
Future<HomeFeed> fetchDiscoverFeed() async => HomeFeed(
sections: [
FeedSection(
code: 'hot',
title: 'Hot',
dramas: [
_drama('zh-demo', 'Chinese Demo', 'zh-CN'),
_drama('id-demo', 'Indonesian Demo', 'id'),
],
),
],
);
}
Drama _drama(String id, String title, String language) => Drama(
id: id,
title: title,
synopsis: '$title synopsis',
posterUrl: '/files/$id.png',
language: language,
detailLoaded: true,
);
+3 -3
View File
@@ -46,11 +46,11 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -520));
await tester.tap(find.byKey(const Key('mine-settings')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('mine-notices')), findsOneWidget);
await tester.tap(find.byKey(const Key('mine-notices')));
expect(find.byKey(const Key('settings-notices')), findsOneWidget);
await tester.tap(find.byKey(const Key('settings-notices')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('notices-page')), findsOneWidget);
+1 -1
View File
@@ -3,7 +3,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/pages/ranking/ranking_logic.dart';
import 'package:fruitshorts/pages/discover/ranking_logic.dart';
void main() {
test('should_use_backend_hot_order_when_hot_board_is_selected', () {
+13 -35
View File
@@ -150,7 +150,6 @@ void main() {
isFalse,
);
expect(find.text('Quick access'), findsOneWidget);
expect(find.text('Bahasa Indonesia'), findsOneWidget);
expect(find.text('7 EPISODES'), findsOneWidget);
expect(tester.takeException(), isNull);
@@ -181,7 +180,7 @@ void main() {
},
);
testWidgets('should_change_interface_language_when_locale_is_picked', (
testWidgets('should_follow_system_locale_without_language_switch', (
tester,
) async {
tester.view.physicalSize = const Size(390, 844);
@@ -196,20 +195,13 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('Vicky sheng'), findsOneWidget);
expect(find.text('Coins'), findsOneWidget);
await tester.drag(find.byType(ListView), const Offset(0, -700));
await tester.pumpAndSettle();
await tester.tap(find.text('App Language'));
await tester.pumpAndSettle();
await tester.tap(find.text('简体中文').last);
await tester.pumpAndSettle();
expect(find.text('我的'), findsWidgets);
await tester.drag(find.byType(ListView), const Offset(0, 700));
await tester.pumpAndSettle();
expect(find.text('App Language'), findsNothing);
expect(find.text('简体中文'), findsNothing);
expect(find.byKey(const Key('mine-wallet')), findsOneWidget);
expect(find.text('首页'), findsOneWidget);
await tester.tap(find.text('发现'));
expect(find.text('Home'), findsWidgets);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.text('推荐'), findsOneWidget);
expect(find.text('Discover'), findsWidgets);
});
testWidgets(
@@ -297,7 +289,8 @@ void main() {
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
expect(find.text('Recommended'), findsOneWidget);
expect(find.byKey(const Key('discover-category-grid')), findsOneWidget);
expect(find.byKey(const Key('discover-ranking-section')), findsOneWidget);
expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget);
expect(find.byKey(const Key('discover-profile')), findsOneWidget);
expect(
@@ -319,11 +312,7 @@ void main() {
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -320));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('mine-library-tab-1')));
await tester.pumpAndSettle();
expect(find.text('No shows here yet.'), findsOneWidget);
expect(find.byKey(const Key('mine-menu')), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget);
@@ -353,16 +342,11 @@ void main() {
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -700));
await tester.pumpAndSettle();
expect(find.text('History'), findsWidgets);
expect(find.byKey(const Key('mine-history')), findsOneWidget);
expect(
Navigator.of(tester.element(find.text('History').first)).canPop(),
Navigator.of(tester.element(find.byKey(const Key('mine-menu')))).canPop(),
isFalse,
);
await tester.tap(find.byKey(const Key('mine-library-tab-1')));
await tester.pumpAndSettle();
expect(find.text('No shows here yet.'), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-discover')));
await tester.pumpAndSettle();
@@ -376,11 +360,7 @@ void main() {
);
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -400));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('mine-library-tab-1')));
await tester.pumpAndSettle();
expect(find.text('No shows here yet.'), findsOneWidget);
expect(find.byKey(const Key('mine-menu')), findsOneWidget);
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget);
@@ -625,9 +605,7 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-my')));
await tester.pumpAndSettle();
await tester.drag(find.byType(ListView), const Offset(0, -320));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('mine-library-tab-1')));
await tester.tap(find.byKey(const Key('mine-watchlist')));
await tester.pumpAndSettle();
expect(find.text('The Signal'), findsOneWidget);
expect(find.text('City Lights & Velvet Nights'), findsWidgets);