Skip to content

Commit 8a40118

Browse files
committed
sticky_header test: Add tests for sticky headers
We're about to replace this library with a fresh implementation that has a different design, in order to gain some features we'll want for the message list. These tests will help us validate the new implementation. Some of the naming in this test code reflects the interface the new implementation will have, in order to changes in the tests and so maximize their effectiveness in validating the reimplementation.
1 parent c25f94d commit 8a40118

File tree

1 file changed

+189
-0
lines changed

1 file changed

+189
-0
lines changed

test/widgets/sticky_header_test.dart

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:checks/checks.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:zulip/widgets/sticky_header.dart';
7+
8+
void main() {
9+
testWidgets('sticky headers: scroll up, headers bounded by items, semi-explicit version', (tester) async {
10+
await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr,
11+
child: StickyHeaderListView(
12+
reverse: true,
13+
children: List.generate(100, (i) => StickyHeaderItem(
14+
header: _Header(i, height: 20),
15+
content: _Item(i, height: 80))))));
16+
17+
void checkState(int index, {required double item, required double header}) =>
18+
_checkHeader(tester, index, first: false,
19+
item: Offset(0, item), header: Offset(0, header));
20+
21+
checkState(5, item: 20, header: 0);
22+
23+
await _drag(tester, const Offset(0, 5));
24+
checkState(6, item: -75, header: -15);
25+
26+
await _drag(tester, const Offset(0, 75));
27+
checkState(6, item: 0, header: 0);
28+
29+
await _drag(tester, const Offset(0, 20));
30+
checkState(6, item: 20, header: 0);
31+
});
32+
33+
for (final reverse in [true, false]) {
34+
for (final reverseHeader in [true, false]) {
35+
final name = 'sticky headers: '
36+
'scroll ${reverse ? 'up' : 'down'}, '
37+
'header at ${reverseHeader ? 'bottom' : 'top'}';
38+
testWidgets(name, (tester) =>
39+
_checkSequence(tester,
40+
Axis.vertical,
41+
reverse: reverse,
42+
reverseHeader: reverseHeader,
43+
));
44+
}
45+
}
46+
47+
for (final reverse in [true, false]) {
48+
for (final reverseHeader in [true, false]) {
49+
for (final textDirection in TextDirection.values) {
50+
final name = 'sticky headers: '
51+
'${textDirection.name.toUpperCase()} '
52+
'scroll ${reverse ? 'backward' : 'forward'}, '
53+
'header at ${reverseHeader ? 'end' : 'start'}';
54+
testWidgets(name, (tester) =>
55+
_checkSequence(tester,
56+
Axis.horizontal, textDirection: textDirection,
57+
reverse: reverse,
58+
reverseHeader: reverseHeader,
59+
));
60+
}
61+
}
62+
}
63+
}
64+
65+
Future<void> _checkSequence(
66+
WidgetTester tester,
67+
Axis axis, {
68+
TextDirection? textDirection,
69+
bool reverse = false,
70+
bool reverseHeader = false,
71+
}) async {
72+
assert(textDirection != null || axis == Axis.vertical);
73+
final headerAtCoordinateEnd = switch (axis) {
74+
Axis.horizontal => reverseHeader ^ (textDirection == TextDirection.rtl),
75+
Axis.vertical => reverseHeader,
76+
};
77+
78+
final controller = ScrollController();
79+
await tester.pumpWidget(Directionality(
80+
textDirection: textDirection ?? TextDirection.rtl,
81+
child: StickyHeaderListView(
82+
controller: controller,
83+
scrollDirection: axis,
84+
reverse: reverse,
85+
children: List.generate(100, (i) => StickyHeaderItem(
86+
direction: switch ((axis, headerAtCoordinateEnd)) {
87+
(Axis.horizontal, true ) => AxisDirection.left,
88+
(Axis.horizontal, false) => AxisDirection.right,
89+
(Axis.vertical, true ) => AxisDirection.up,
90+
(Axis.vertical, false) => AxisDirection.down,
91+
},
92+
header: _Header(i, height: 20),
93+
content: _Item(i, height: 80))))));
94+
95+
final extent = tester.getSize(find.byType(StickyHeaderListView)).onAxis(axis);
96+
assert(extent % 100 == 0);
97+
98+
final first = !(reverse ^ reverseHeader);
99+
100+
final itemFinder = first ? find.byType(_Item).first : find.byType(_Item).last;
101+
final headerFinder = first ? find.byType(_Header).first : find.byType(_Header).last;
102+
103+
double insetExtent(Finder finder) {
104+
return headerAtCoordinateEnd
105+
? extent - tester.getTopLeft(finder).inDirection(axis.coordinateDirection)
106+
: tester.getBottomRight(finder).inDirection(axis.coordinateDirection);
107+
}
108+
109+
void checkState() {
110+
final scrollOffset = controller.position.pixels;
111+
final expectedHeaderIndex = first
112+
? (scrollOffset / 100).floor()
113+
: (extent ~/ 100 - 1) + (scrollOffset / 100).ceil();
114+
check(tester.widget<_Item>(itemFinder).index).equals(expectedHeaderIndex);
115+
check(tester.widget<_Header>(headerFinder).index).equals(expectedHeaderIndex);
116+
117+
final expectedItemInsetExtent =
118+
100 - (first ? scrollOffset % 100 : (-scrollOffset) % 100);
119+
check(insetExtent(itemFinder)).equals(expectedItemInsetExtent);
120+
check(insetExtent(headerFinder)).equals(
121+
math.min(20, expectedItemInsetExtent));
122+
}
123+
124+
Future<void> jumpAndCheck(double position) async {
125+
controller.jumpTo(position);
126+
await tester.pump();
127+
checkState();
128+
}
129+
130+
checkState();
131+
await jumpAndCheck(5);
132+
await jumpAndCheck(10);
133+
await jumpAndCheck(20);
134+
await jumpAndCheck(50);
135+
await jumpAndCheck(80);
136+
await jumpAndCheck(90);
137+
await jumpAndCheck(95);
138+
await jumpAndCheck(100);
139+
}
140+
141+
Future<void> _drag(WidgetTester tester, Offset offset) async {
142+
await tester.drag(find.byType(StickyHeaderListView), offset);
143+
await tester.pump();
144+
}
145+
146+
void _checkHeader(
147+
WidgetTester tester,
148+
int index, {
149+
required bool first,
150+
required Offset item,
151+
required Offset header,
152+
}) {
153+
final itemFinder = first ? find.byType(_Item).first : find.byType(_Item).last;
154+
final headerFinder = first ? find.byType(_Header).first : find.byType(_Header).last;
155+
check(tester.widget<_Item>(itemFinder).index).equals(index);
156+
check(tester.widget<_Header>(headerFinder).index).equals(index);
157+
check(tester.getTopLeft(itemFinder)).equals(item);
158+
check(tester.getTopLeft(headerFinder)).equals(header);
159+
}
160+
161+
class _Header extends StatelessWidget {
162+
const _Header(this.index, {required this.height});
163+
164+
final int index;
165+
final double height;
166+
167+
@override
168+
Widget build(BuildContext context) {
169+
return SizedBox(
170+
height: height,
171+
width: height, // TODO clean up
172+
child: Text("Header $index"));
173+
}
174+
}
175+
176+
class _Item extends StatelessWidget {
177+
const _Item(this.index, {required this.height});
178+
179+
final int index;
180+
final double height;
181+
182+
@override
183+
Widget build(BuildContext context) {
184+
return SizedBox(
185+
height: height,
186+
width: height,
187+
child: Text("Item $index"));
188+
}
189+
}

0 commit comments

Comments
 (0)