// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../rendering/rendering_tester.dart' show TestClipPaintingContext; import 'states.dart'; void main() { // Regression test for https://github.com/flutter/flutter/issues/100451 testWidgets('GridView.builder respects findChildIndexCallback', (WidgetTester tester) async { bool finderCalled = false; int itemCount = 7; late StateSetter stateSetter; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { stateSetter = setState; return GridView.builder( itemCount: itemCount, itemBuilder: (BuildContext _, int index) => Container( key: Key('$index'), height: 2000.0, ), findChildIndexCallback: (Key key) { finderCalled = true; return null; }, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), ); }, ), ) ); expect(finderCalled, false); // Trigger update. stateSetter(() => itemCount = 77); await tester.pump(); expect(finderCalled, true); }); testWidgets('Empty GridView', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.count( dragStartBehavior: DragStartBehavior.down, crossAxisCount: 4, ), ), ); }); testWidgets('GridView.count control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.count( dragStartBehavior: DragStartBehavior.down, crossAxisCount: 4, children: kStates.map<Widget>((String state) { return GestureDetector( dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, child: ColoredBox( color: const Color(0xFF0000FF), child: Text(state), ), ); }).toList(), ), ), ); expect(tester.getSize(find.text('Arkansas')), equals(const Size(200.0, 200.0))); for (int i = 0; i < 8; ++i) { await tester.tap(find.text(kStates[i])); expect(log, equals(<String>[kStates[i]])); log.clear(); } expect(find.text(kStates[12]), findsNothing); expect(find.text('Nevada'), findsNothing); await tester.drag(find.text('Arkansas'), const Offset(0.0, -200.0)); await tester.pump(); for (int i = 0; i < 4; ++i) { expect(find.text(kStates[i]), findsNothing); } for (int i = 4; i < 12; ++i) { await tester.tap(find.text(kStates[i])); expect(log, equals(<String>[kStates[i]])); log.clear(); } await tester.drag(find.text('Delaware'), const Offset(0.0, -4000.0)); await tester.pump(); expect(find.text('Alabama'), findsNothing); expect(find.text('Pennsylvania'), findsNothing); expect(tester.getCenter(find.text('Tennessee')), equals(const Offset(300.0, 100.0))); await tester.tap(find.text('Tennessee')); expect(log, equals(<String>['Tennessee'])); log.clear(); await tester.drag(find.text('Tennessee'), const Offset(0.0, 200.0)); await tester.pump(); await tester.tap(find.text('Tennessee')); expect(log, equals(<String>['Tennessee'])); log.clear(); await tester.tap(find.text('Pennsylvania')); expect(log, equals(<String>['Pennsylvania'])); log.clear(); }); testWidgets('GridView.extent control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.extent( dragStartBehavior: DragStartBehavior.down, maxCrossAxisExtent: 200.0, children: kStates.map<Widget>((String state) { return GestureDetector( dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, child: ColoredBox( color: const Color(0xFF0000FF), child: Text(state), ), ); }).toList(), ), ), ); expect(tester.getSize(find.text('Arkansas')), equals(const Size(200.0, 200.0))); for (int i = 0; i < 8; ++i) { await tester.tap(find.text(kStates[i])); expect(log, equals(<String>[kStates[i]])); log.clear(); } expect(find.text('Nevada'), findsNothing); await tester.drag(find.text('Arkansas'), const Offset(0.0, -4000.0)); await tester.pump(); expect(find.text('Alabama'), findsNothing); expect(tester.getCenter(find.text('Tennessee')), equals(const Offset(300.0, 100.0))); await tester.tap(find.text('Tennessee')); expect(log, equals(<String>['Tennessee'])); log.clear(); }); testWidgets('GridView large scroll jump', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.extent( scrollDirection: Axis.horizontal, maxCrossAxisExtent: 200.0, childAspectRatio: 0.75, children: List<Widget>.generate(80, (int i) { return Builder( builder: (BuildContext context) { log.add(i); return Text('$i'); }, ); }), ), ), ); expect(tester.getSize(find.text('4')), equals(const Size(200.0 / 0.75, 200.0))); expect(log, equals(<int>[ 0, 1, 2, // col 0 3, 4, 5, // col 1 6, 7, 8, // col 2 9, 10, 11, // col 3 (in cached area) ])); log.clear(); for (int i = 0; i < 9; i++) { expect(find.text('$i'), findsOneWidget); } for (int i = 9; i < 80; i++) { expect(find.text('$i'), findsNothing); } final ScrollableState state = tester.state(find.byType(Scrollable)); final ScrollPosition position = state.position; position.jumpTo(3025.0); expect(log, isEmpty); await tester.pump(); expect(log, equals(<int>[ 30, 31, 32, // col 10 (in cached area) 33, 34, 35, // col 11 36, 37, 38, // col 12 39, 40, 41, // col 13 42, 43, 44, // col 14 45, 46, 47, // col 15 (in cached area) ])); log.clear(); for (int i = 0; i < 33; i++) { expect(find.text('$i'), findsNothing); } for (int i = 33; i < 45; i++) { expect(find.text('$i'), findsOneWidget); } for (int i = 45; i < 80; i++) { expect(find.text('$i'), findsNothing); } position.jumpTo(975.0); expect(log, isEmpty); await tester.pump(); expect(log, equals(<int>[ 6, 7, 8, // col2 (in cached area) 9, 10, 11, // col 3 12, 13, 14, // col 4 15, 16, 17, // col 5 18, 19, 20, // col 6 21, 22, 23, // col 7 (in cached area) ])); log.clear(); for (int i = 0; i < 9; i++) { expect(find.text('$i'), findsNothing); } for (int i = 9; i < 21; i++) { expect(find.text('$i'), findsOneWidget); } for (int i = 21; i < 80; i++) { expect(find.text('$i'), findsNothing); } }); testWidgets('GridView - change crossAxisCount', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), children: List<Widget>.generate(40, (int i) { return Builder( builder: (BuildContext context) { log.add(i); return Text('$i'); }, ); }), ), ), ); expect(tester.getSize(find.text('4')), equals(const Size(200.0, 200.0))); expect(log, equals(<int>[ 0, 1, 2, 3, // row 0 4, 5, 6, 7, // row 1 8, 9, 10, 11, // row 2 12, 13, 14, 15, // row 3 (in cached area) 16, 17, 18, 19, // row 4 (in cached area) ])); for (int i = 0; i < 12; i++) { expect(find.text('$i'), findsOneWidget); } for (int i = 12; i < 40; i++) { expect(find.text('$i'), findsNothing); } log.clear(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), children: List<Widget>.generate(40, (int i) { return Builder( builder: (BuildContext context) { log.add(i); return Text('$i'); }, ); }), ), ), ); expect(log, equals(<int>[ 0, 1, 2, 3, // row 0 4, 5, 6, 7, // row 1 8, 9, 10, 11, // row 2 12, 13, 14, 15, // row 3 (in cached area) 16, 17, 18, 19, // row 4 (in cached area) ])); log.clear(); expect(tester.getSize(find.text('3')), equals(const Size(400.0, 400.0))); expect(find.text('4'), findsNothing); }); testWidgets('SliverGridRegularTileLayout - can handle close to zero mainAxisStride', (WidgetTester tester) async { const SliverGridDelegateWithMaxCrossAxisExtent delegate = SliverGridDelegateWithMaxCrossAxisExtent( childAspectRatio: 1e300, maxCrossAxisExtent: 500.0, ); final SliverGridLayout layout = delegate.getLayout( const SliverConstraints( axisDirection: AxisDirection.down, growthDirection: GrowthDirection.forward, userScrollDirection: ScrollDirection.forward, scrollOffset: 100.0, precedingScrollExtent: 0.0, overlap: 0.0, remainingPaintExtent: 0.0, crossAxisExtent: 500, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 100.0, remainingCacheExtent: 0.0, cacheOrigin: 0.0, ), ); expect(layout.getMinChildIndexForScrollOffset(1000.0), 0.0); }); testWidgets('GridView - change maxChildCrossAxisExtent', (WidgetTester tester) async { final List<int> log = <int>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200.0, ), children: List<Widget>.generate(40, (int i) { return Builder( builder: (BuildContext context) { log.add(i); return Text('$i'); }, ); }), ), ), ); expect(tester.getSize(find.text('4')), equals(const Size(200.0, 200.0))); expect(log, equals(<int>[ 0, 1, 2, 3, // row 0 4, 5, 6, 7, // row 1 8, 9, 10, 11, // row 2 12, 13, 14, 15, // row 3 (in cached area) 16, 17, 18, 19, // row 4 (in cached area) ])); for (int i = 0; i < 12; i++) { expect(find.text('$i'), findsOneWidget); } for (int i = 12; i < 40; i++) { expect(find.text('$i'), findsNothing); } log.clear(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 400.0, ), children: List<Widget>.generate(40, (int i) { return Builder( builder: (BuildContext context) { log.add(i); return Text('$i'); }, ); }), ), ), ); expect(log, equals(<int>[ 0, 1, 2, 3, // row 0 4, 5, 6, 7, // row 1 8, 9, 10, 11, // row 2 12, 13, 14, 15, // row 3 (in cached area) 16, 17, 18, 19, // row 4 (in cached area) ])); log.clear(); expect(tester.getSize(find.text('3')), equals(const Size(400.0, 400.0))); expect(find.text('4'), findsNothing); }); testWidgets('One-line GridView paints', (WidgetTester tester) async { const Color green = Color(0xFF00FF00); final Container container = Container( decoration: const BoxDecoration( color: green, ), ); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 200.0, child: GridView.count( cacheExtent: 0.0, crossAxisCount: 2, children: <Widget>[ container, container, container, container ], ), ), ), ), ); expect(find.byType(GridView), paints..rect(color: green)..rect(color: green)); expect(find.byType(GridView), isNot(paints..rect(color: green)..rect(color: green)..rect(color: green))); }); testWidgets('GridView in zero context', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox.shrink( child: GridView.count( crossAxisCount: 4, children: List<Widget>.generate(20, (int i) { return Text('$i'); }), ), ), ), ), ); expect(find.text('0'), findsNothing); expect(find.text('1'), findsNothing); }); testWidgets('GridView in unbounded context', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( child: GridView.count( crossAxisCount: 4, shrinkWrap: true, children: List<Widget>.generate(20, (int i) { return Text('$i'); }), ), ), ), ); expect(find.text('0'), findsOneWidget); expect(find.text('19'), findsOneWidget); }); testWidgets('GridView.builder control test', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), shrinkWrap: true, itemCount: 20, itemBuilder: (BuildContext context, int index) { return Text('$index'); }, ), ), ); expect(find.text('0'), findsOneWidget); expect(find.text('11'), findsOneWidget); expect(find.text('12'), findsNothing); }); testWidgets('GridView.builder with undefined itemCount', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, ), shrinkWrap: true, itemBuilder: (BuildContext context, int index) { return Text('$index'); }, ), ), ); expect(find.text('0'), findsOneWidget); expect(find.text('11'), findsOneWidget); await tester.drag(find.byType(GridView), const Offset(0.0, -300.0)); await tester.pump(const Duration(milliseconds: 200)); expect(find.text('13'), findsOneWidget); }); testWidgets('GridView cross axis layout', (WidgetTester tester) async { final Key target = UniqueKey(); Widget build(TextDirection textDirection) { return Directionality( textDirection: textDirection, child: GridView.count( crossAxisCount: 4, children: <Widget>[ Container(key: target), ], ), ); } await tester.pumpWidget(build(TextDirection.ltr)); expect(tester.getTopLeft(find.byKey(target)), Offset.zero); expect(tester.getBottomRight(find.byKey(target)), const Offset(200.0, 200.0)); await tester.pumpWidget(build(TextDirection.rtl)); expect(tester.getTopLeft(find.byKey(target)), const Offset(600.0, 0.0)); expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 200.0)); }); testWidgets('GridView crossAxisSpacing', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/27151. final Key target = UniqueKey(); Widget build(TextDirection textDirection) { return Directionality( textDirection: textDirection, child: GridView.count( crossAxisCount: 4, crossAxisSpacing: 8.0, children: <Widget>[ Container(key: target), ], ), ); } await tester.pumpWidget(build(TextDirection.ltr)); expect(tester.getTopLeft(find.byKey(target)), Offset.zero); expect(tester.getBottomRight(find.byKey(target)), const Offset(194.0, 194.0)); await tester.pumpWidget(build(TextDirection.rtl)); expect(tester.getTopLeft(find.byKey(target)), const Offset(606.0, 0.0)); expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 194.0)); }); testWidgets('GridView does not cache itemBuilder calls', (WidgetTester tester) async { final Map<int, int> counters = <int, int>{}; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: GridView.builder( itemCount: 1000, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemBuilder: (BuildContext context, int index) { counters[index] = (counters[index] ?? 0) + 1; return SizedBox( key: ValueKey<int>(index), width: 200, height: 200, ); }, ), )); expect(find.byKey(const ValueKey<int>(4)), findsOneWidget); expect(counters[4], 1); await tester.fling(find.byType(GridView), const Offset(0, -300), 5000); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey<int>(4)), findsNothing); expect(counters[4], 1); await tester.fling(find.byType(GridView), const Offset(0, 300), 5000); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey<int>(4)), findsOneWidget); expect(counters[4], 2); }); testWidgets('GridView does not report visual overflow unnecessarily', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), children: <Widget>[ Container(height: 200.0), ], ), ), ); // 1st, check that the render object has received the default clip behavior. final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.hardEdge)); // The context will get Clip.none because there is no actual visual overflow. final TestClipPaintingContext context = TestClipPaintingContext(); renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.none)); }); testWidgets('GridView respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), children: <Widget>[ Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), ], ), ), ); // 1st, check that the render object has received the default clip behavior. final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.hardEdge)); // 2nd, check that the painting context has received the default clip behavior. final TestClipPaintingContext context = TestClipPaintingContext(); renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.hardEdge)); // 3rd, pump a new widget to check that the render object can update its clip behavior. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), clipBehavior: Clip.antiAlias, children: <Widget>[ Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), Container(height: 2000.0), ], ), ), ); expect(renderObject.clipBehavior, equals(Clip.antiAlias)); // 4th, check that a non-default clip behavior can be sent to the painting context. renderObject.paint(context, Offset.zero); expect(context.clipBehavior, equals(Clip.antiAlias)); }); testWidgets('GridView.builder respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemCount: 10, itemBuilder: (BuildContext _, int __) => Container(height: 2000.0), clipBehavior: Clip.antiAlias, ), ), ); final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); testWidgets('GridView.custom respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.custom( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), childrenDelegate: SliverChildBuilderDelegate( (BuildContext context, int index) => Container(height: 2000.0), childCount: 1, ), clipBehavior: Clip.antiAlias, ), ), ); final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); testWidgets('GridView.count respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.count( crossAxisCount: 3, clipBehavior: Clip.antiAlias, children: <Widget>[Container(height: 2000.0)], ), ), ); final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); testWidgets('GridView.extent respects clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView.extent( maxCrossAxisExtent: 1000, clipBehavior: Clip.antiAlias, children: <Widget>[Container(height: 2000.0)], ), ), ); final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; expect(renderObject.clipBehavior, equals(Clip.antiAlias)); }); testWidgets('SliverGridDelegateWithFixedCrossAxisCount mainAxisExtent works as expected', (WidgetTester tester) async { const int crossAxisCount = 4; const double mainAxisExtent = 100.0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, mainAxisExtent: mainAxisExtent, ), children: List<Widget>.generate(20, (int i) { return Builder( builder: (BuildContext context) { return Text('$i'); }, ); }), ), ), ); expect(tester.getSize(find.text('4')), equals(const Size(200.0, mainAxisExtent))); }); testWidgets('SliverGridDelegateWithMaxCrossAxisExtent mainAxisExtent works as expected', (WidgetTester tester) async { const double maxCrossAxisExtent = 200.0; const double mainAxisExtent = 100.0; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: GridView( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: maxCrossAxisExtent, mainAxisExtent: mainAxisExtent, ), children: List<Widget>.generate(20, (int i) { return Builder( builder: (BuildContext context) { return Text('$i'); }, ); }), ), ), ); expect(tester.getSize(find.text('4')), equals(const Size(200.0, mainAxisExtent))); }); testWidgets('SliverGridDelegateWithMaxCrossAxisExtent throws assertion error when maxCrossAxisExtent is 0', (WidgetTester tester) async { const double maxCrossAxisExtent = 0; expect(() => Directionality( textDirection: TextDirection.ltr, child: GridView.extent( maxCrossAxisExtent: maxCrossAxisExtent, ), ), throwsAssertionError); }); }