@@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart' hide SliverPaintOrder;
6
6
import 'package:flutter_test/flutter_test.dart' ;
7
7
import 'package:zulip/widgets/scrolling.dart' ;
8
8
9
+ import '../flutter_checks.dart' ;
10
+
9
11
void main () {
10
12
group ('CustomPaintOrderScrollView paint order' , () {
11
13
final paintLog = < int > [];
@@ -124,6 +126,126 @@ void main() {
124
126
// the former matched the latter's old default behavior.
125
127
);
126
128
});
129
+
130
+ group ('MessageListScrollView' , () {
131
+ Widget buildList ({
132
+ MessageListScrollController ? controller,
133
+ required double topHeight,
134
+ required double bottomHeight,
135
+ }) {
136
+ return MessageListScrollView (
137
+ controller: controller ?? MessageListScrollController (),
138
+ center: const ValueKey ('center' ),
139
+ slivers: [
140
+ SliverToBoxAdapter (
141
+ child: SizedBox (height: topHeight, child: Text ('top' ))),
142
+ SliverToBoxAdapter (key: const ValueKey ('center' ),
143
+ child: SizedBox (height: bottomHeight, child: Text ('bottom' ))),
144
+ ]);
145
+ }
146
+
147
+ Future <void > prepare (WidgetTester tester, {
148
+ MessageListScrollController ? controller,
149
+ required double topHeight,
150
+ required double bottomHeight,
151
+ }) async {
152
+ await tester.pumpWidget (Directionality (textDirection: TextDirection .ltr,
153
+ child: buildList (controller: controller,
154
+ topHeight: topHeight, bottomHeight: bottomHeight)));
155
+ await tester.pump ();
156
+ }
157
+
158
+ // The `skipOffstage: false` produces more informative output
159
+ // when a test fails because one of the slivers is just offscreen.
160
+ final findTop = find.text ('top' , skipOffstage: false );
161
+ final findBottom = find.text ('bottom' , skipOffstage: false );
162
+
163
+ testWidgets ('short/short -> starts scrolled to bottom' , (tester) async {
164
+ // Starts out with items at bottom of viewport.
165
+ await prepare (tester, topHeight: 100 , bottomHeight: 100 );
166
+ check (tester.getRect (findBottom)).bottom.equals (600 );
167
+
168
+ // Try scrolling down (by dragging up); doesn't move.
169
+ await tester.drag (findTop, Offset (0 , - 100 ));
170
+ await tester.pump ();
171
+ check (tester.getRect (findBottom)).bottom.equals (600 );
172
+ });
173
+
174
+ testWidgets ('short/long -> starts scrolled to bottom' , (tester) async {
175
+ // Starts out scrolled to bottom.
176
+ await prepare (tester, topHeight: 100 , bottomHeight: 800 );
177
+ check (tester.getRect (findBottom)).bottom.equals (600 );
178
+
179
+ // Try scrolling down (by dragging up); doesn't move.
180
+ await tester.drag (findBottom, Offset (0 , - 100 ));
181
+ await tester.pump ();
182
+ check (tester.getRect (findBottom)).bottom.equals (600 );
183
+ });
184
+
185
+ testWidgets ('starts at bottom, even when bottom underestimated at first' , (tester) async {
186
+ const numItems = 10 ;
187
+ const itemHeight = 300.0 ;
188
+
189
+ // A list where the bottom sliver takes several rounds of layout
190
+ // to see how long it really is.
191
+ final controller = MessageListScrollController ();
192
+ await tester.pumpWidget (Directionality (textDirection: TextDirection .ltr,
193
+ child: MessageListScrollView (
194
+ controller: controller,
195
+ center: const ValueKey ('center' ),
196
+ slivers: [
197
+ SliverToBoxAdapter (
198
+ child: SizedBox (height: 100 , child: Text ('top' ))),
199
+ SliverList .list (key: const ValueKey ('center' ),
200
+ children: List .generate (numItems, (i) =>
201
+ SizedBox (height: (i+ 1 ) * itemHeight, child: Text ('item $i ' )))),
202
+ ])));
203
+ await tester.pump ();
204
+
205
+ // Starts out scrolled all the way to the bottom,
206
+ // even though it must have taken several rounds of layout to find that.
207
+ check (controller.position.pixels)
208
+ .equals (itemHeight * numItems * (numItems + 1 )/ 2 );
209
+ check (tester.getRect (find.text ('item ${numItems -1 }' , skipOffstage: false )))
210
+ .bottom.equals (600 );
211
+ });
212
+
213
+ testWidgets ('position preserved when scrollable rebuilds' , (tester) async {
214
+ // Tests that [MessageListScrollPosition.absorb] does its job.
215
+ //
216
+ // In the app, this situation can be triggered by changing the device's
217
+ // theme between light and dark. For this simplified example for a test,
218
+ // go for devicePixelRatio (which ScrollableState directly depends on).
219
+
220
+ final controller = MessageListScrollController ();
221
+ final widget = Directionality (textDirection: TextDirection .ltr,
222
+ child: buildList (controller: controller,
223
+ topHeight: 400 , bottomHeight: 400 ));
224
+ await tester.pumpWidget (
225
+ MediaQuery (data: MediaQueryData (devicePixelRatio: 1.0 ),
226
+ child: widget));
227
+ check (tester.getRect (findTop)).bottom.equals (200 );
228
+ final position = controller.position;
229
+ check (position).isA <MessageListScrollPosition >();
230
+
231
+ // Drag away from the initial scroll position.
232
+ await tester.drag (findBottom, Offset (0 , 200 ));
233
+ await tester.pump ();
234
+ check (tester.getRect (findTop)).bottom.equals (400 );
235
+ check (controller.position).identicalTo (position);
236
+
237
+ // Then cause the ScrollableState to have didChangeDependencies called…
238
+ await tester.pumpWidget (
239
+ MediaQuery (data: MediaQueryData (devicePixelRatio: 2.0 ),
240
+ child: widget));
241
+ // … so that it constructs a new MessageListScrollPosition…
242
+ check (controller.position)
243
+ ..not ((it) => it.identicalTo (position))
244
+ ..isA <MessageListScrollPosition >();
245
+ // … and check the scroll position is preserved, not reset to initial.
246
+ check (tester.getRect (findTop)).bottom.equals (400 );
247
+ });
248
+ });
127
249
}
128
250
129
251
class TestCustomPainter extends CustomPainter {
0 commit comments