Skip to content

Commit 1907fba

Browse files
committed
sticky_header test: Test slivers splitting viewport
There are still some bugs affecting the sticky_header library when a SliverStickyHeaderList occupies only part of the viewport (which is a configuration we'll need for letting the message list grow in both directions, for #82). I sent a PR which aimed to fix a cluster of those, in which I tried to get away without writing these systematic test cases for them. It worked for the cases I did test -- including the cases that would actually arise for the Zulip message list -- and I believed the changes were correct when I sent the PR. But that version was still conceptually confused, as evidenced by the fact that it turned out to break other cases: #1316 (comment) So that seems like a sign that this really should get systematic all-cases tests. Some of these new test cases don't yet work properly, because they exercise the aforementioned bugs. The "header overflowing sliver" skip condition will be removed later in this series. The "paint order" skips will be addressed in an upcoming PR.
1 parent 50f7eec commit 1907fba

File tree

1 file changed

+139
-30
lines changed

1 file changed

+139
-30
lines changed

test/widgets/sticky_header_test.dart

Lines changed: 139 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
44
import 'package:collection/collection.dart';
55
import 'package:flutter/foundation.dart';
66
import 'package:flutter/gestures.dart';
7+
import 'package:flutter/rendering.dart';
78
import 'package:flutter/widgets.dart';
89
import 'package:flutter_test/flutter_test.dart';
910
import 'package:zulip/widgets/sticky_header.dart';
@@ -75,36 +76,42 @@ void main() {
7576
for (final reverse in [true, false]) {
7677
for (final reverseHeader in [true, false]) {
7778
for (final growthDirection in GrowthDirection.values) {
78-
for (final allowOverflow in [true, false]) {
79-
final name = 'sticky headers: '
80-
'scroll ${reverse ? 'up' : 'down'}, '
81-
'header at ${reverseHeader ? 'bottom' : 'top'}, '
82-
'$growthDirection, '
83-
'headers ${allowOverflow ? 'overflow' : 'bounded'}';
84-
testWidgets(name, (tester) =>
85-
_checkSequence(tester,
86-
Axis.vertical,
87-
reverse: reverse,
88-
reverseHeader: reverseHeader,
89-
growthDirection: growthDirection,
90-
allowOverflow: allowOverflow,
91-
));
92-
93-
for (final textDirection in TextDirection.values) {
79+
for (final sliverConfig in _SliverConfig.values) {
80+
for (final allowOverflow in [true, false]) {
9481
final name = 'sticky headers: '
95-
'${textDirection.name.toUpperCase()} '
96-
'scroll ${reverse ? 'backward' : 'forward'}, '
97-
'header at ${reverseHeader ? 'end' : 'start'}, '
82+
'scroll ${reverse ? 'up' : 'down'}, '
83+
'header at ${reverseHeader ? 'bottom' : 'top'}, '
9884
'$growthDirection, '
99-
'headers ${allowOverflow ? 'overflow' : 'bounded'}';
85+
'headers ${allowOverflow ? 'overflow' : 'bounded'}, '
86+
'slivers ${sliverConfig.name}';
10087
testWidgets(name, (tester) =>
10188
_checkSequence(tester,
102-
Axis.horizontal, textDirection: textDirection,
89+
Axis.vertical,
10390
reverse: reverse,
10491
reverseHeader: reverseHeader,
10592
growthDirection: growthDirection,
10693
allowOverflow: allowOverflow,
94+
sliverConfig: sliverConfig,
10795
));
96+
97+
for (final textDirection in TextDirection.values) {
98+
final name = 'sticky headers: '
99+
'${textDirection.name.toUpperCase()} '
100+
'scroll ${reverse ? 'backward' : 'forward'}, '
101+
'header at ${reverseHeader ? 'end' : 'start'}, '
102+
'$growthDirection, '
103+
'headers ${allowOverflow ? 'overflow' : 'bounded'}, '
104+
'slivers ${sliverConfig.name}';
105+
testWidgets(name, (tester) =>
106+
_checkSequence(tester,
107+
Axis.horizontal, textDirection: textDirection,
108+
reverse: reverse,
109+
reverseHeader: reverseHeader,
110+
growthDirection: growthDirection,
111+
allowOverflow: allowOverflow,
112+
sliverConfig: sliverConfig,
113+
));
114+
}
108115
}
109116
}
110117
}
@@ -223,6 +230,12 @@ void main() {
223230
});
224231
}
225232

233+
enum _SliverConfig {
234+
single,
235+
backToBack,
236+
followed,
237+
}
238+
226239
Future<void> _checkSequence(
227240
WidgetTester tester,
228241
Axis axis, {
@@ -231,6 +244,7 @@ Future<void> _checkSequence(
231244
bool reverseHeader = false,
232245
GrowthDirection growthDirection = GrowthDirection.forward,
233246
required bool allowOverflow,
247+
_SliverConfig sliverConfig = _SliverConfig.single,
234248
}) async {
235249
assert(textDirection != null || axis == Axis.vertical);
236250
final headerAtCoordinateEnd = switch (axis) {
@@ -241,32 +255,80 @@ Future<void> _checkSequence(
241255
final headerPlacement = reverseHeader ^ reverse
242256
? HeaderPlacement.scrollingEnd : HeaderPlacement.scrollingStart;
243257

258+
if (allowOverflow
259+
&& ((sliverConfig == _SliverConfig.backToBack
260+
&& (reverse ^ reverseHeader))
261+
|| (sliverConfig == _SliverConfig.followed
262+
&& (reverse ^ reverseHeader ^ !reverseGrowth)))) {
263+
// (The condition for this skip is pretty complicated; it's just the
264+
// conditions where the bug gets triggered, and I haven't tried to
265+
// work through why this exact set of cases is what's affected.
266+
// The important thing is they all get fixed in an upcoming commit.)
267+
markTestSkipped('bug in header overflowing sliver'); // TODO fix
268+
return;
269+
}
270+
244271
Widget buildItem(int i) {
245272
return StickyHeaderItem(
246273
allowOverflow: allowOverflow,
247274
header: _Header(i, height: 20),
248275
child: _Item(i, height: 100));
249276
}
250277

278+
const sliverScrollExtent = 1000;
251279
const center = ValueKey("center");
252280
final slivers = <Widget>[
281+
if (sliverConfig == _SliverConfig.backToBack)
282+
SliverStickyHeaderList(
283+
headerPlacement: headerPlacement,
284+
delegate: SliverChildListDelegate(
285+
List.generate(10, (i) => buildItem(-i - 1)))),
253286
const SliverPadding(
254287
key: center,
255288
padding: EdgeInsets.zero),
256289
SliverStickyHeaderList(
257290
headerPlacement: headerPlacement,
258291
delegate: SliverChildListDelegate(
259292
List.generate(10, (i) => buildItem(i)))),
293+
if (sliverConfig == _SliverConfig.followed)
294+
SliverStickyHeaderList(
295+
headerPlacement: headerPlacement,
296+
delegate: SliverChildListDelegate(
297+
List.generate(10, (i) => buildItem(i + 10)))),
260298
];
261299

262300
final double anchor;
301+
bool paintOrderGood;
263302
if (reverseGrowth) {
264303
slivers.reverseRange(0, slivers.length);
265304
anchor = 1.0;
305+
paintOrderGood = switch (sliverConfig) {
306+
_SliverConfig.single => true,
307+
// The last sliver will paint last.
308+
_SliverConfig.backToBack => headerPlacement == HeaderPlacement.scrollingEnd,
309+
// The last sliver will paint last.
310+
_SliverConfig.followed => headerPlacement == HeaderPlacement.scrollingEnd,
311+
};
266312
} else {
267313
anchor = 0.0;
314+
paintOrderGood = switch (sliverConfig) {
315+
_SliverConfig.single => true,
316+
// The last sliver will paint last.
317+
_SliverConfig.backToBack => headerPlacement == HeaderPlacement.scrollingEnd,
318+
// The first sliver will paint last.
319+
_SliverConfig.followed => headerPlacement == HeaderPlacement.scrollingStart,
320+
};
321+
}
322+
323+
final skipBecausePaintOrder = allowOverflow && !paintOrderGood;
324+
if (skipBecausePaintOrder) {
325+
// TODO need to control paint order of slivers within viewport in order to
326+
// make some configurations behave properly when headers overflow slivers
327+
markTestSkipped('sliver paint order');
328+
// Don't return yet; we'll still check layout, and skip specific affected checks below.
268329
}
269330

331+
270332
final controller = ScrollController();
271333
await tester.pumpWidget(Directionality(
272334
textDirection: textDirection ?? TextDirection.rtl,
@@ -281,6 +343,7 @@ Future<void> _checkSequence(
281343
final overallSize = tester.getSize(find.byType(CustomScrollView));
282344
final extent = overallSize.onAxis(axis);
283345
assert(extent % 100 == 0);
346+
assert(sliverScrollExtent - extent > 100);
284347

285348
// A position `inset` from the center of the edge the header is found on.
286349
Offset headerInset(double inset) {
@@ -318,6 +381,7 @@ Future<void> _checkSequence(
318381
check(insetExtent(find.byType(_Header))).equals(expectedHeaderInsetExtent);
319382

320383
// Check the header gets hit when it should, and not when it shouldn't.
384+
if (skipBecausePaintOrder) return;
321385
await tester.tapAt(headerInset(1));
322386
await tester.tapAt(headerInset(expectedHeaderInsetExtent - 1));
323387
check(_TapLogged.takeTapLog())..length.equals(2)
@@ -335,15 +399,60 @@ Future<void> _checkSequence(
335399
await checkState();
336400
}
337401

338-
await checkState();
339-
await jumpAndCheck(5);
340-
await jumpAndCheck(10);
341-
await jumpAndCheck(20);
342-
await jumpAndCheck(50);
343-
await jumpAndCheck(80);
344-
await jumpAndCheck(90);
345-
await jumpAndCheck(95);
346-
await jumpAndCheck(100);
402+
Future<void> checkLocally() async {
403+
final scrollOffset = controller.position.pixels * (reverseGrowth ? -1 : 1);
404+
await checkState();
405+
await jumpAndCheck(scrollOffset + 5);
406+
await jumpAndCheck(scrollOffset + 10);
407+
await jumpAndCheck(scrollOffset + 20);
408+
await jumpAndCheck(scrollOffset + 50);
409+
await jumpAndCheck(scrollOffset + 80);
410+
await jumpAndCheck(scrollOffset + 90);
411+
await jumpAndCheck(scrollOffset + 95);
412+
await jumpAndCheck(scrollOffset + 100);
413+
}
414+
415+
Iterable<double> listExtents() {
416+
final result = tester.renderObjectList(find.byType(SliverStickyHeaderList, skipOffstage: false))
417+
.map((renderObject) => (renderObject as RenderSliver)
418+
.geometry!.layoutExtent);
419+
return reverseGrowth ? result.toList().reversed : result;
420+
}
421+
422+
switch (sliverConfig) {
423+
case _SliverConfig.single:
424+
// Just check the first header, at a variety of offsets,
425+
// and check it hands off to the next header.
426+
await checkLocally();
427+
428+
case _SliverConfig.followed:
429+
// Check behavior as the next sliver scrolls into view.
430+
await jumpAndCheck(sliverScrollExtent - extent);
431+
check(listExtents()).deepEquals([extent, 0]);
432+
await checkLocally();
433+
check(listExtents()).deepEquals([extent - 100, 100]);
434+
435+
// Check behavior as the original sliver scrolls out of view.
436+
await jumpAndCheck(sliverScrollExtent - 100);
437+
check(listExtents()).deepEquals([100, extent - 100]);
438+
await checkLocally();
439+
check(listExtents()).deepEquals([0, extent]);
440+
441+
case _SliverConfig.backToBack:
442+
// Scroll the other sliver into view;
443+
// check behavior as it scrolls back out.
444+
await jumpAndCheck(-100);
445+
check(listExtents()).deepEquals([100, extent - 100]);
446+
await checkLocally();
447+
check(listExtents()).deepEquals([0, extent]);
448+
449+
// Scroll the original sliver out of view;
450+
// check behavior as it scrolls back in.
451+
await jumpAndCheck(-extent);
452+
check(listExtents()).deepEquals([extent, 0]);
453+
await checkLocally();
454+
check(listExtents()).deepEquals([extent - 100, 100]);
455+
}
347456
}
348457

349458
abstract class _SelectItemFinder extends FinderBase<Element> with ChainedFinderMixin<Element> {

0 commit comments

Comments
 (0)