-
Notifications
You must be signed in to change notification settings - Fork 305
/
Copy pathscrolling_test.dart
330 lines (281 loc) · 12.6 KB
/
scrolling_test.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import 'package:checks/checks.dart';
// ignore: undefined_hidden_name // anticipates https://github.com/flutter/flutter/pull/164818
import 'package:flutter/rendering.dart' hide SliverPaintOrder;
// ignore: undefined_hidden_name // anticipates https://github.com/flutter/flutter/pull/164818
import 'package:flutter/widgets.dart' hide SliverPaintOrder;
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/widgets/scrolling.dart';
import '../flutter_checks.dart';
void main() {
group('CustomPaintOrderScrollView paint order', () {
final paintLog = <int>[];
Widget makeSliver(int i) {
return SliverToBoxAdapter(
key: ValueKey(i),
child: CustomPaint(
painter: TestCustomPainter()
..onPaint = (_, _) => paintLog.add(i),
child: Text('Item $i')));
}
testWidgets('firstIsTop', (tester) async {
addTearDown(paintLog.clear);
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.firstIsTop,
center: ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
// First sliver paints last, over other slivers; last sliver paints first.
check(paintLog).deepEquals([4, 3, 2, 1, 0]);
});
testWidgets('lastIsTop', (tester) async {
addTearDown(paintLog.clear);
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.lastIsTop,
center: ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
// Last sliver paints last, over other slivers; first sliver paints first.
check(paintLog).deepEquals([0, 1, 2, 3, 4]);
});
// This test will fail if a corresponding upstream PR lands:
// https://github.com/flutter/flutter/pull/164818
// because that eliminates the quirky centerTopFirstBottom behavior.
// In that case, skip this test for a quick fix; or go ahead and
// rip out CustomPaintOrderScrollView in favor of CustomScrollView.
// (Greg has a draft commit ready which does the latter.)
testWidgets('centerTopFirstBottom', (tester) async {
addTearDown(paintLog.clear);
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.centerTopFirstBottom,
center: ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
// The particular order CustomScrollView paints in.
check(paintLog).deepEquals([0, 1, 4, 3, 2]);
// Check that CustomScrollView indeed paints in the same order.
final result = paintLog.toList();
paintLog.clear();
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomScrollView(
center: ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
check(paintLog).deepEquals(result);
});
});
group('CustomPaintOrderScrollView hit-test order', () {
Widget makeSliver(int i) {
return _AllOverlapSliver(key: ValueKey<int>(i), id: i);
}
List<int> sliverIds(Iterable<HitTestEntry> path) => [
for (final e in path)
if (e.target case _RenderAllOverlapSliver(:final id))
id,
];
testWidgets('firstIsTop', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.firstIsTop,
center: const ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
final result = tester.hitTestOnBinding(const Offset(400, 300));
check(sliverIds(result.path)).deepEquals([0, 1, 2, 3, 4]);
});
testWidgets('lastIsTop', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.lastIsTop,
center: const ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
final result = tester.hitTestOnBinding(const Offset(400, 300));
check(sliverIds(result.path)).deepEquals([4, 3, 2, 1, 0]);
});
// This test will fail if the upstream PR 164818 lands.
// In that case the test is no longer needed and we'll take it out;
// see comment on other centerTopFirstBottom test above.
testWidgets('centerTopFirstBottom', (tester) async {
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomPaintOrderScrollView(
paintOrder: SliverPaintOrder.centerTopFirstBottom,
center: const ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
final result = tester.hitTestOnBinding(const Offset(400, 300));
// The particular order CustomScrollView hit-tests in.
check(sliverIds(result.path)).deepEquals([2, 3, 4, 1, 0]);
// Check that CustomScrollView indeed hit-tests in the same order.
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: CustomScrollView(
center: const ValueKey(2), anchor: 0.5,
slivers: List.generate(5, makeSliver))));
check(sliverIds(tester.hitTestOnBinding(const Offset(400, 300)).path))
.deepEquals(sliverIds(result.path));
});
});
group('MessageListScrollView', () {
Future<void> prepare(WidgetTester tester, {
MessageListScrollController? controller,
required double topHeight,
required double bottomHeight,
}) async {
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: MessageListScrollView(
controller: controller ?? MessageListScrollController(),
center: const ValueKey('center'),
slivers: [
SliverToBoxAdapter(
child: SizedBox(height: topHeight, child: Text('top'))),
SliverToBoxAdapter(key: const ValueKey('center'),
child: SizedBox(height: bottomHeight, child: Text('bottom'))),
])));
await tester.pump();
}
// The `skipOffstage: false` produces more informative output
// when a test fails because one of the slivers is just offscreen.
final findTop = find.text('top', skipOffstage: false);
final findBottom = find.text('bottom', skipOffstage: false);
testWidgets('short/short -> pinned at bottom', (tester) async {
// Starts out with items at bottom of viewport.
await prepare(tester, topHeight: 100, bottomHeight: 100);
check(tester.getRect(findBottom)).bottom.equals(600);
// Try scrolling down (by dragging up); doesn't move.
await tester.drag(findTop, Offset(0, -100));
await tester.pump();
check(tester.getRect(findBottom)).bottom.equals(600);
// Try scrolling up (by dragging down); doesn't move.
await tester.drag(findTop, Offset(0, 100));
await tester.pump();
check(tester.getRect(findBottom)).bottom.equals(600);
});
testWidgets('short/long -> scrolls to ends and no farther', (tester) async {
// Starts out scrolled to bottom.
await prepare(tester, topHeight: 100, bottomHeight: 800);
check(tester.getRect(findBottom)).bottom.equals(600);
// Try scrolling down (by dragging up); doesn't move.
await tester.drag(findBottom, Offset(0, -100));
await tester.pump();
check(tester.getRect(findBottom)).bottom.equals(600);
// Try scrolling up (by dragging down); moves only as far as top of list.
await tester.drag(findBottom, Offset(0, 400));
await tester.pump();
check(tester.getRect(findBottom)).bottom.equals(900);
check(tester.getRect(findTop)).top.equals(0);
});
testWidgets('short/short -> starts at bottom, immediately without animation', (tester) async {
await prepare(tester, topHeight: 100, bottomHeight: 100);
final ys = <double>[];
for (int i = 0; i < 10; i++) {
ys.add(tester.getRect(findBottom).bottom - 600);
await tester.pump(Duration(milliseconds: 15));
}
check(ys).deepEquals(List.generate(10, (_) => 0.0));
});
testWidgets('short/long -> starts at bottom, immediately without animation', (tester) async {
await prepare(tester, topHeight: 100, bottomHeight: 800);
final ys = <double>[];
for (int i = 0; i < 10; i++) {
ys.add(tester.getRect(findBottom).bottom - 600);
await tester.pump(Duration(milliseconds: 15));
}
check(ys).deepEquals(List.generate(10, (_) => 0.0));
});
testWidgets('starts at bottom, even when bottom underestimated at first', (tester) async {
const numItems = 10;
const itemHeight = 300.0;
// A list where the bottom sliver takes several rounds of layout
// to see how long it really is.
final controller = MessageListScrollController();
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
child: MessageListScrollView(
controller: controller,
center: const ValueKey('center'),
slivers: [
SliverToBoxAdapter(
child: SizedBox(height: 100, child: Text('top'))),
SliverList.list(key: const ValueKey('center'),
children: List.generate(numItems, (i) =>
SizedBox(height: (i+1) * itemHeight, child: Text('item $i')))),
])));
await tester.pump();
// Starts out scrolled all the way to the bottom,
// even though it must have taken several rounds of layout to find that.
check(controller.position.pixels)
.equals(itemHeight * numItems * (numItems + 1)/2);
check(tester.getRect(find.text('item ${numItems-1}', skipOffstage: false)))
.bottom.equals(600);
});
testWidgets('stick to end of list when it grows', (tester) async {
final controller = MessageListScrollController();
await prepare(tester, controller: controller,
topHeight: 400, bottomHeight: 400);
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
// Bottom sliver grows; remain scrolled to (new) bottom.
await prepare(tester, controller: controller,
topHeight: 400, bottomHeight: 500);
check(tester.getRect(findBottom))..top.equals(100)..bottom.equals(600);
});
testWidgets('when not at end, let it grow without following', (tester) async {
final controller = MessageListScrollController();
await prepare(tester, controller: controller,
topHeight: 400, bottomHeight: 400);
check(tester.getRect(findBottom))..top.equals(200)..bottom.equals(600);
// Scroll up (by dragging down) to detach from end of list.
await tester.drag(findBottom, Offset(0, 100));
await tester.pump();
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(700);
// Bottom sliver grows; remain at existing position, now farther from end.
await prepare(tester, controller: controller,
topHeight: 400, bottomHeight: 500);
check(tester.getRect(findBottom))..top.equals(300)..bottom.equals(800);
});
});
}
class TestCustomPainter extends CustomPainter {
void Function(Canvas canvas, Size size)? onPaint;
@override
void paint(Canvas canvas, Size size) {
if (onPaint != null) onPaint!(canvas, size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
/// A sliver that overlaps with other slivers as far as possible,
/// and does nothing else.
class _AllOverlapSliver extends LeafRenderObjectWidget {
const _AllOverlapSliver({super.key, required this.id});
final int id;
@override
RenderObject createRenderObject(BuildContext context) => _RenderAllOverlapSliver(id);
}
class _RenderAllOverlapSliver extends RenderSliver {
_RenderAllOverlapSliver(this.id);
final int id;
@override
void performLayout() {
geometry = SliverGeometry(
paintExtent: constraints.remainingPaintExtent,
maxPaintExtent: constraints.remainingPaintExtent,
layoutExtent: 0.0,
);
}
@override
bool hitTest(
SliverHitTestResult result, {
required double mainAxisPosition,
required double crossAxisPosition,
}) {
if (mainAxisPosition >= 0.0 &&
mainAxisPosition < geometry!.hitTestExtent &&
crossAxisPosition >= 0.0 &&
crossAxisPosition < constraints.crossAxisExtent) {
result.add(
SliverHitTestEntry(
this,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition,
),
);
}
return false;
}
}