|
1 | 1 | import 'dart:async'; |
2 | 2 | import 'package:flutter/material.dart'; |
| 3 | +import 'package:flutter/rendering.dart' show ScrollCacheExtent; |
3 | 4 | import 'package:flutter_test/flutter_test.dart'; |
4 | 5 | import 'package:scroll_infinity/scroll_infinity.dart'; |
5 | 6 |
|
@@ -823,6 +824,345 @@ void main() { |
823 | 824 | }, |
824 | 825 | ); |
825 | 826 | }); |
| 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 | + }); |
826 | 1166 | }); |
827 | 1167 | } |
828 | 1168 |
|
|
0 commit comments