Skip to content

Commit f940a08

Browse files
test: cover onError, onItemsLoaded, controller,
`pull-to-refresh`, and `ListView` passthrough - Adds test coverage for `onError`, `onItemsLoaded`, `ScrollInfinityController`, `enablePullToRefresh`, `physics`/`shrinkWrap`/`primary`/`cacheExtent`, and `loadMoreThreshold`. - Uses `fvm flutter test` because the `flutter` binary in `PATH` has mismatched engine and framework versions in this environment.
1 parent 53d399c commit f940a08

1 file changed

Lines changed: 340 additions & 0 deletions

File tree

test/src/scroll_infinity_test.dart

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22
import 'package:flutter/material.dart';
3+
import 'package:flutter/rendering.dart' show ScrollCacheExtent;
34
import 'package:flutter_test/flutter_test.dart';
45
import 'package:scroll_infinity/scroll_infinity.dart';
56

@@ -823,6 +824,345 @@ void main() {
823824
},
824825
);
825826
});
827+
828+
/// Tests for the `onError` and `onItemsLoaded` analytics hooks.
829+
group('Analytics Callbacks', () {
830+
testWidgets(
831+
'onError receives the exception thrown by loadData',
832+
(tester) async {
833+
Object? capturedError;
834+
835+
await tester.pumpWidget(
836+
buildTestableWidget(
837+
ScrollInfinity<String>(
838+
maxItems: 10,
839+
onError: (error) => capturedError = error,
840+
loadData: (page) {
841+
return mockLoadData(page, throwErrorOnPage: true);
842+
},
843+
itemBuilder: (item, index) => Text(item),
844+
),
845+
),
846+
);
847+
848+
await tester.pumpAndSettle();
849+
850+
expect(capturedError, isA<Exception>());
851+
expect(capturedError.toString(), contains('Failed to load data'));
852+
},
853+
);
854+
855+
testWidgets(
856+
'onError is not called when loadData succeeds',
857+
(tester) async {
858+
var called = false;
859+
860+
await tester.pumpWidget(
861+
buildTestableWidget(
862+
ScrollInfinity<String>(
863+
maxItems: 5,
864+
onError: (_) => called = true,
865+
loadData: (page) {
866+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
867+
},
868+
itemBuilder: (item, index) => Text(item),
869+
),
870+
),
871+
);
872+
873+
await tester.pumpAndSettle();
874+
875+
expect(called, isFalse);
876+
},
877+
);
878+
879+
testWidgets(
880+
'onItemsLoaded receives the raw items returned by loadData',
881+
(tester) async {
882+
final loadedBatches = <List<String>>[];
883+
884+
await tester.pumpWidget(
885+
buildTestableWidget(
886+
ScrollInfinity<String>(
887+
maxItems: 5,
888+
onItemsLoaded: loadedBatches.add,
889+
loadData: (page) {
890+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
891+
},
892+
itemBuilder: (item, index) {
893+
return SizedBox(height: 150, child: Text(item));
894+
},
895+
),
896+
),
897+
);
898+
899+
await tester.pumpAndSettle();
900+
901+
expect(loadedBatches, [
902+
['Item 0', 'Item 1', 'Item 2', 'Item 3', 'Item 4'],
903+
]);
904+
},
905+
);
906+
907+
testWidgets(
908+
'onItemsLoaded is not called when loadData fails',
909+
(tester) async {
910+
var called = false;
911+
912+
await tester.pumpWidget(
913+
buildTestableWidget(
914+
ScrollInfinity<String>(
915+
maxItems: 10,
916+
onItemsLoaded: (_) => called = true,
917+
loadData: (page) {
918+
return mockLoadData(page, throwErrorOnPage: true);
919+
},
920+
itemBuilder: (item, index) => Text(item),
921+
),
922+
),
923+
);
924+
925+
await tester.pumpAndSettle();
926+
927+
expect(called, isFalse);
928+
},
929+
);
930+
});
931+
932+
/// Tests for driving a mounted [ScrollInfinity] via
933+
/// [ScrollInfinityController].
934+
group('External Controller', () {
935+
testWidgets(
936+
'Reflects isLoading/hasError and refresh() resets the list',
937+
(tester) async {
938+
final controller = ScrollInfinityController();
939+
var callCount = 0;
940+
941+
await tester.pumpWidget(
942+
buildTestableWidget(
943+
ScrollInfinity<String>(
944+
maxItems: 5,
945+
controller: controller,
946+
loadData: (page) {
947+
callCount++;
948+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
949+
},
950+
itemBuilder: (item, index) {
951+
return SizedBox(height: 150, child: Text(item));
952+
},
953+
),
954+
),
955+
);
956+
957+
expect(controller.isLoading, isTrue);
958+
await tester.pumpAndSettle();
959+
expect(controller.isLoading, isFalse);
960+
expect(controller.hasError, isFalse);
961+
expect(callCount, 1);
962+
963+
controller.refresh();
964+
await tester.pump();
965+
expect(controller.isLoading, isTrue);
966+
967+
await tester.pumpAndSettle();
968+
expect(callCount, 2);
969+
expect(find.text('Item 0'), findsOneWidget);
970+
},
971+
);
972+
973+
testWidgets(
974+
'retry() re-fetches after an error',
975+
(tester) async {
976+
final controller = ScrollInfinityController();
977+
var callCount = 0;
978+
979+
await tester.pumpWidget(
980+
buildTestableWidget(
981+
ScrollInfinity<String>(
982+
maxItems: 10,
983+
controller: controller,
984+
loadData: (page) {
985+
callCount++;
986+
return mockLoadData(page, throwErrorOnPage: true);
987+
},
988+
itemBuilder: (item, index) => Text(item),
989+
),
990+
),
991+
);
992+
993+
await tester.pumpAndSettle();
994+
expect(controller.hasError, isTrue);
995+
expect(callCount, 1);
996+
997+
controller.retry();
998+
await tester.pumpAndSettle();
999+
1000+
expect(callCount, 2);
1001+
},
1002+
);
1003+
1004+
testWidgets(
1005+
'Becomes inert without throwing after ScrollInfinity is disposed',
1006+
(tester) async {
1007+
final controller = ScrollInfinityController();
1008+
1009+
await tester.pumpWidget(
1010+
buildTestableWidget(
1011+
ScrollInfinity<String>(
1012+
maxItems: 5,
1013+
controller: controller,
1014+
loadData: (page) {
1015+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
1016+
},
1017+
itemBuilder: (item, index) => Text(item),
1018+
),
1019+
),
1020+
);
1021+
await tester.pumpAndSettle();
1022+
1023+
await tester.pumpWidget(const SizedBox());
1024+
1025+
expect(controller.refresh, returnsNormally);
1026+
expect(controller.isLoading, isFalse);
1027+
expect(controller.hasError, isFalse);
1028+
},
1029+
);
1030+
});
1031+
1032+
/// Tests for the `enablePullToRefresh` property.
1033+
group('Pull To Refresh', () {
1034+
testWidgets(
1035+
'Does not wrap the list in a RefreshIndicator by default',
1036+
(tester) async {
1037+
await tester.pumpWidget(
1038+
buildTestableWidget(
1039+
ScrollInfinity<String>(
1040+
maxItems: 5,
1041+
loadData: (page) {
1042+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
1043+
},
1044+
itemBuilder: (item, index) => Text(item),
1045+
),
1046+
),
1047+
);
1048+
1049+
await tester.pumpAndSettle();
1050+
1051+
expect(find.byType(RefreshIndicator), findsNothing);
1052+
},
1053+
);
1054+
1055+
testWidgets(
1056+
'enablePullToRefresh wraps the list and refetches on pull',
1057+
(tester) async {
1058+
var callCount = 0;
1059+
1060+
await tester.pumpWidget(
1061+
buildTestableWidget(
1062+
ScrollInfinity<String>(
1063+
maxItems: 5,
1064+
enablePullToRefresh: true,
1065+
loadData: (page) {
1066+
callCount++;
1067+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
1068+
},
1069+
itemBuilder: (item, index) {
1070+
return SizedBox(height: 150, child: Text(item));
1071+
},
1072+
),
1073+
),
1074+
);
1075+
1076+
await tester.pumpAndSettle();
1077+
expect(find.byType(RefreshIndicator), findsOneWidget);
1078+
expect(callCount, 1);
1079+
1080+
await tester.fling(
1081+
find.byType(ListView),
1082+
const Offset(0, 300),
1083+
1000,
1084+
);
1085+
await tester.pumpAndSettle();
1086+
1087+
expect(callCount, 2);
1088+
},
1089+
);
1090+
});
1091+
1092+
/// Tests for properties passed straight through to the underlying
1093+
/// [ListView].
1094+
group('ListView Passthrough', () {
1095+
testWidgets(
1096+
'Applies physics, shrinkWrap, primary and cacheExtent',
1097+
(tester) async {
1098+
const physics = BouncingScrollPhysics();
1099+
1100+
await tester.pumpWidget(
1101+
buildTestableWidget(
1102+
ScrollInfinity<String>(
1103+
maxItems: 5,
1104+
physics: physics,
1105+
shrinkWrap: true,
1106+
primary: false,
1107+
cacheExtent: 500,
1108+
loadData: (page) {
1109+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
1110+
},
1111+
itemBuilder: (item, index) => Text(item),
1112+
),
1113+
),
1114+
);
1115+
1116+
await tester.pumpAndSettle();
1117+
1118+
final listView = tester.widget<ListView>(find.byType(ListView));
1119+
expect(listView.physics, physics);
1120+
expect(listView.shrinkWrap, isTrue);
1121+
expect(listView.primary, isFalse);
1122+
expect(
1123+
listView.scrollCacheExtent,
1124+
const ScrollCacheExtent.pixels(500),
1125+
);
1126+
},
1127+
);
1128+
1129+
testWidgets(
1130+
'Leaves scrollCacheExtent null when cacheExtent is not set',
1131+
(tester) async {
1132+
await tester.pumpWidget(
1133+
buildTestableWidget(
1134+
ScrollInfinity<String>(
1135+
maxItems: 5,
1136+
loadData: (page) {
1137+
return mockLoadData(page, totalItems: 5, maxItemsPerPage: 5);
1138+
},
1139+
itemBuilder: (item, index) => Text(item),
1140+
),
1141+
),
1142+
);
1143+
1144+
await tester.pumpAndSettle();
1145+
1146+
final listView = tester.widget<ListView>(find.byType(ListView));
1147+
expect(listView.scrollCacheExtent, isNull);
1148+
},
1149+
);
1150+
});
1151+
1152+
/// Tests for the `loadMoreThreshold` property.
1153+
group('Configuration Validation', () {
1154+
test('loadMoreThreshold cannot be negative', () {
1155+
expect(
1156+
() => ScrollInfinity<String>(
1157+
maxItems: 5,
1158+
loadMoreThreshold: -1,
1159+
loadData: (page) async => [],
1160+
itemBuilder: (item, index) => Text(item),
1161+
),
1162+
throwsAssertionError,
1163+
);
1164+
});
1165+
});
8261166
});
8271167
}
8281168

0 commit comments

Comments
 (0)