368 lines
11 KiB
Dart
368 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:fruitshorts/core/data/services/category_controller.dart';
|
|
import 'package:fruitshorts/core/data/services/home_feed_controller.dart';
|
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
|
import 'package:fruitshorts/pages/home/home_feed_channel.dart';
|
|
import 'package:fruitshorts/pages/recommend/recommend_page.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../support/fake_catalog.dart';
|
|
import '../support/fake_category.dart';
|
|
|
|
void main() {
|
|
testWidgets('should_show_skeleton_before_feed_is_loaded', (tester) async {
|
|
await _pumpRecommend(tester, active: false);
|
|
|
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsOneWidget);
|
|
expect(find.byKey(const Key('recommend-feed-empty')), findsNothing);
|
|
expect(find.text('Retry'), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_refresh_once_when_parent_rebuilds_repeatedly', (
|
|
tester,
|
|
) async {
|
|
final fixture = await _pumpRecommend(tester, active: true);
|
|
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
for (var index = 0; index < 20; index++) {
|
|
fixture.harnessKey.currentState!.rebuildSameInputs();
|
|
await tester.pump();
|
|
}
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
});
|
|
|
|
testWidgets('should_refresh_once_when_active_turns_on', (tester) async {
|
|
final fixture = await _pumpRecommend(tester, active: false);
|
|
expect(fixture.controller.refreshCalls, 0);
|
|
|
|
fixture.harnessKey.currentState!.update(active: true);
|
|
await tester.pump();
|
|
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
fixture.harnessKey.currentState!.rebuildSameInputs();
|
|
await tester.pump();
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
});
|
|
|
|
testWidgets('should_refresh_again_when_data_source_changes', (tester) async {
|
|
final fixture = await _pumpRecommend(tester, active: true);
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
|
|
fixture.harnessKey.currentState!.update(active: false);
|
|
await tester.pump();
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
|
|
fixture.harnessKey.currentState!.update(active: true);
|
|
await tester.pump();
|
|
expect(fixture.controller.refreshCalls, 2);
|
|
});
|
|
|
|
testWidgets('should_allow_manual_retry_after_auto_retries_are_exhausted', (
|
|
tester,
|
|
) async {
|
|
final fixture = await _pumpFailingRecommend(tester);
|
|
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsOneWidget);
|
|
expect(find.byKey(const Key('recommend-feed-empty')), findsNothing);
|
|
|
|
for (var attempt = 0; attempt < 3; attempt++) {
|
|
await tester.pump(const Duration(milliseconds: 1500));
|
|
await tester.pump();
|
|
expect(fixture.controller.refreshCalls, attempt + 2);
|
|
if (attempt < 2) {
|
|
expect(
|
|
find.byKey(const Key('recommend-feed-skeleton')),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byKey(const Key('recommend-feed-empty')), findsNothing);
|
|
}
|
|
}
|
|
|
|
final l10n = AppLocalizations.of(
|
|
tester.element(find.byType(RecommendPage)),
|
|
)!;
|
|
expect(fixture.controller.refreshCalls, 4);
|
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsNothing);
|
|
expect(find.byKey(const Key('recommend-feed-empty')), findsOneWidget);
|
|
expect(find.text(l10n.networkError), findsOneWidget);
|
|
expect(find.text(l10n.retry), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('recommend-feed-retry')));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
|
|
expect(fixture.controller.refreshCalls, 5);
|
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_cancel_pending_auto_retry_when_disposed', (tester) async {
|
|
final fixture = await _pumpFailingRecommend(tester);
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
|
|
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
|
|
await tester.pump(const Duration(seconds: 2));
|
|
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
});
|
|
|
|
testWidgets(
|
|
'should_show_no_stories_without_retry_when_loaded_feed_is_empty',
|
|
(tester) async {
|
|
final fixture = await _pumpEmptyRecommend(tester);
|
|
|
|
final l10n = AppLocalizations.of(
|
|
tester.element(find.byType(RecommendPage)),
|
|
)!;
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
expect(find.byKey(const Key('recommend-feed-skeleton')), findsNothing);
|
|
expect(find.byKey(const Key('recommend-feed-empty')), findsOneWidget);
|
|
expect(find.text(l10n.noStories), findsOneWidget);
|
|
expect(find.text(l10n.retry), findsNothing);
|
|
},
|
|
);
|
|
|
|
testWidgets('should_not_refresh_after_dispose_when_channel_changes', (
|
|
tester,
|
|
) async {
|
|
final fixture = await _pumpRecommend(tester, active: true);
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
|
|
fixture.harnessKey.currentState!.update(active: true);
|
|
final selection = fixture.controller.selectChannel(HomeFeedChannel.anime);
|
|
await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink()));
|
|
await selection;
|
|
await tester.pump();
|
|
|
|
expect(fixture.controller.refreshCalls, 1);
|
|
});
|
|
}
|
|
|
|
Future<_RecommendFixture> _pumpRecommend(
|
|
WidgetTester tester, {
|
|
required bool active,
|
|
}) async {
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final catalog = FakeCatalogRepository();
|
|
final controller = _RecordingHomeFeedController(catalog);
|
|
final categories = CategoryController(
|
|
FakeCategoryRepository(catalog: catalog),
|
|
);
|
|
final harnessKey = GlobalKey<_RecommendHarnessState>();
|
|
final fixture = _RecommendFixture(
|
|
controller: controller,
|
|
categoryController: categories,
|
|
harnessKey: harnessKey,
|
|
);
|
|
addTearDown(controller.dispose);
|
|
addTearDown(categories.dispose);
|
|
|
|
await tester.pumpWidget(fixture.app(active: active));
|
|
await tester.pump();
|
|
return fixture;
|
|
}
|
|
|
|
Future<_StatefulRecommendFixture> _pumpFailingRecommend(
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final catalog = FakeCatalogRepository();
|
|
final controller = _StatefulHomeFeedController(catalog);
|
|
final categories = CategoryController(
|
|
FakeCategoryRepository(catalog: catalog),
|
|
);
|
|
addTearDown(controller.dispose);
|
|
addTearDown(categories.dispose);
|
|
|
|
final fixture = _StatefulRecommendFixture(
|
|
controller: controller,
|
|
categoryController: categories,
|
|
);
|
|
await tester.pumpWidget(fixture.app());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
return fixture;
|
|
}
|
|
|
|
Future<_StatefulRecommendFixture> _pumpEmptyRecommend(
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(390, 844);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
final catalog = FakeCatalogRepository();
|
|
final controller = _StatefulHomeFeedController(catalog, fail: false);
|
|
final categories = CategoryController(
|
|
FakeCategoryRepository(catalog: catalog),
|
|
);
|
|
addTearDown(controller.dispose);
|
|
addTearDown(categories.dispose);
|
|
|
|
final fixture = _StatefulRecommendFixture(
|
|
controller: controller,
|
|
categoryController: categories,
|
|
);
|
|
await tester.pumpWidget(fixture.app());
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
return fixture;
|
|
}
|
|
|
|
class _RecommendFixture {
|
|
const _RecommendFixture({
|
|
required this.controller,
|
|
required this.categoryController,
|
|
required this.harnessKey,
|
|
});
|
|
|
|
final _RecordingHomeFeedController controller;
|
|
final CategoryController categoryController;
|
|
final GlobalKey<_RecommendHarnessState> harnessKey;
|
|
|
|
Widget app({required bool active}) {
|
|
return MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: ChangeNotifierProvider<CategoryController>.value(
|
|
value: categoryController,
|
|
child: _RecommendHarness(
|
|
key: harnessKey,
|
|
controller: controller,
|
|
active: active,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _StatefulRecommendFixture {
|
|
const _StatefulRecommendFixture({
|
|
required this.controller,
|
|
required this.categoryController,
|
|
});
|
|
|
|
final _StatefulHomeFeedController controller;
|
|
final CategoryController categoryController;
|
|
|
|
Widget app() {
|
|
return MaterialApp(
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: ChangeNotifierProvider<CategoryController>.value(
|
|
value: categoryController,
|
|
child: RecommendPage(
|
|
feedController: controller,
|
|
active: true,
|
|
videoOnly: true,
|
|
onSearch: () {},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RecommendHarness extends StatefulWidget {
|
|
const _RecommendHarness({
|
|
super.key,
|
|
required this.controller,
|
|
required this.active,
|
|
});
|
|
|
|
final HomeFeedController controller;
|
|
final bool active;
|
|
|
|
@override
|
|
State<_RecommendHarness> createState() => _RecommendHarnessState();
|
|
}
|
|
|
|
class _RecommendHarnessState extends State<_RecommendHarness> {
|
|
late HomeFeedController _controller;
|
|
late bool _active;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = widget.controller;
|
|
_active = widget.active;
|
|
}
|
|
|
|
void rebuildSameInputs() {
|
|
setState(() {});
|
|
}
|
|
|
|
void update({required bool active}) {
|
|
setState(() {
|
|
_active = active;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return RecommendPage(
|
|
feedController: _controller,
|
|
active: _active,
|
|
videoOnly: true,
|
|
onSearch: () {},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RecordingHomeFeedController extends HomeFeedController {
|
|
_RecordingHomeFeedController(super.repository);
|
|
|
|
int refreshCalls = 0;
|
|
|
|
@override
|
|
Future<bool> refreshCurrent() async {
|
|
refreshCalls++;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class _StatefulHomeFeedController extends HomeFeedController {
|
|
_StatefulHomeFeedController(super.repository, {this.fail = true});
|
|
|
|
final bool fail;
|
|
int refreshCalls = 0;
|
|
bool _loaded = false;
|
|
bool _isLoading = false;
|
|
String? _lastError;
|
|
|
|
@override
|
|
bool get loaded => _loaded;
|
|
|
|
@override
|
|
bool get isLoading => _isLoading;
|
|
|
|
@override
|
|
String? get lastError => _lastError;
|
|
|
|
@override
|
|
Future<bool> refreshCurrent() async {
|
|
refreshCalls++;
|
|
_isLoading = true;
|
|
_lastError = null;
|
|
notifyListeners();
|
|
await Future<void>.delayed(Duration.zero);
|
|
_isLoading = false;
|
|
_loaded = true;
|
|
if (fail) _lastError = 'network error';
|
|
notifyListeners();
|
|
return !fail;
|
|
}
|
|
}
|