@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
4
4
import 'package:collection/collection.dart' ;
5
5
import 'package:flutter/foundation.dart' ;
6
6
import 'package:flutter/gestures.dart' ;
7
+ import 'package:flutter/rendering.dart' ;
7
8
import 'package:flutter/widgets.dart' ;
8
9
import 'package:flutter_test/flutter_test.dart' ;
9
10
import 'package:zulip/widgets/sticky_header.dart' ;
@@ -75,36 +76,42 @@ void main() {
75
76
for (final reverse in [true , false ]) {
76
77
for (final reverseHeader in [true , false ]) {
77
78
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 ]) {
94
81
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' }, '
98
84
'$growthDirection , '
99
- 'headers ${allowOverflow ? 'overflow' : 'bounded' }' ;
85
+ 'headers ${allowOverflow ? 'overflow' : 'bounded' }, '
86
+ 'slivers ${sliverConfig .name }' ;
100
87
testWidgets (name, (tester) =>
101
88
_checkSequence (tester,
102
- Axis .horizontal, textDirection : textDirection ,
89
+ Axis .vertical ,
103
90
reverse: reverse,
104
91
reverseHeader: reverseHeader,
105
92
growthDirection: growthDirection,
106
93
allowOverflow: allowOverflow,
94
+ sliverConfig: sliverConfig,
107
95
));
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
+ }
108
115
}
109
116
}
110
117
}
@@ -223,6 +230,12 @@ void main() {
223
230
});
224
231
}
225
232
233
+ enum _SliverConfig {
234
+ single,
235
+ backToBack,
236
+ followed,
237
+ }
238
+
226
239
Future <void > _checkSequence (
227
240
WidgetTester tester,
228
241
Axis axis, {
@@ -231,6 +244,7 @@ Future<void> _checkSequence(
231
244
bool reverseHeader = false ,
232
245
GrowthDirection growthDirection = GrowthDirection .forward,
233
246
required bool allowOverflow,
247
+ _SliverConfig sliverConfig = _SliverConfig .single,
234
248
}) async {
235
249
assert (textDirection != null || axis == Axis .vertical);
236
250
final headerAtCoordinateEnd = switch (axis) {
@@ -241,32 +255,80 @@ Future<void> _checkSequence(
241
255
final headerPlacement = reverseHeader ^ reverse
242
256
? HeaderPlacement .scrollingEnd : HeaderPlacement .scrollingStart;
243
257
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
+
244
271
Widget buildItem (int i) {
245
272
return StickyHeaderItem (
246
273
allowOverflow: allowOverflow,
247
274
header: _Header (i, height: 20 ),
248
275
child: _Item (i, height: 100 ));
249
276
}
250
277
278
+ const sliverScrollExtent = 1000 ;
251
279
const center = ValueKey ("center" );
252
280
final slivers = < Widget > [
281
+ if (sliverConfig == _SliverConfig .backToBack)
282
+ SliverStickyHeaderList (
283
+ headerPlacement: headerPlacement,
284
+ delegate: SliverChildListDelegate (
285
+ List .generate (10 , (i) => buildItem (- i - 1 )))),
253
286
const SliverPadding (
254
287
key: center,
255
288
padding: EdgeInsets .zero),
256
289
SliverStickyHeaderList (
257
290
headerPlacement: headerPlacement,
258
291
delegate: SliverChildListDelegate (
259
292
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 )))),
260
298
];
261
299
262
300
final double anchor;
301
+ bool paintOrderGood;
263
302
if (reverseGrowth) {
264
303
slivers.reverseRange (0 , slivers.length);
265
304
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
+ };
266
312
} else {
267
313
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.
268
329
}
269
330
331
+
270
332
final controller = ScrollController ();
271
333
await tester.pumpWidget (Directionality (
272
334
textDirection: textDirection ?? TextDirection .rtl,
@@ -281,6 +343,7 @@ Future<void> _checkSequence(
281
343
final overallSize = tester.getSize (find.byType (CustomScrollView ));
282
344
final extent = overallSize.onAxis (axis);
283
345
assert (extent % 100 == 0 );
346
+ assert (sliverScrollExtent - extent > 100 );
284
347
285
348
// A position `inset` from the center of the edge the header is found on.
286
349
Offset headerInset (double inset) {
@@ -318,6 +381,7 @@ Future<void> _checkSequence(
318
381
check (insetExtent (find.byType (_Header ))).equals (expectedHeaderInsetExtent);
319
382
320
383
// Check the header gets hit when it should, and not when it shouldn't.
384
+ if (skipBecausePaintOrder) return ;
321
385
await tester.tapAt (headerInset (1 ));
322
386
await tester.tapAt (headerInset (expectedHeaderInsetExtent - 1 ));
323
387
check (_TapLogged .takeTapLog ())..length.equals (2 )
@@ -335,15 +399,60 @@ Future<void> _checkSequence(
335
399
await checkState ();
336
400
}
337
401
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
+ }
347
456
}
348
457
349
458
abstract class _SelectItemFinder extends FinderBase <Element > with ChainedFinderMixin <Element > {
0 commit comments