@@ -4,6 +4,7 @@ import 'package:checks/checks.dart';
44import 'package:collection/collection.dart' ;
55import 'package:flutter/foundation.dart' ;
66import 'package:flutter/gestures.dart' ;
7+ import 'package:flutter/rendering.dart' ;
78import 'package:flutter/widgets.dart' ;
89import 'package:flutter_test/flutter_test.dart' ;
910import '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+
226239Future <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
349458abstract class _SelectItemFinder extends FinderBase <Element > with ChainedFinderMixin <Element > {
0 commit comments