feat(app): 剧场页九宫格与极简我的页
This commit is contained in:
1 parent
dc8ca7b743
commit
35338cbfc8
46 files changed
+2353
-4358
No files matched your search
@@ -1,90 +0,0 @@
|
||||
// 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/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 '../support/fake_catalog.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_filter_theater_grid_when_language_chip_is_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await tester.pumpWidget(
|
||||
LumaDramaApp(
|
||||
playerFactory: createWidgetPlayer,
|
||||
catalogRepository: FakeCatalogRepository(),
|
||||
categoryRepository: _MixedLanguageCategoryRepository(),
|
||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||
favoriteRepository: InMemoryFavoriteRepository(),
|
||||
playbackRepository: FakePlaybackRepository(),
|
||||
commentRepository: FakeCommentRepository(),
|
||||
interactionRepository: FakeInteractionRepository(),
|
||||
noticeRepository: FakeNoticeRepository(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
@@ -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++);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user