| // 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/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'test_widgets.dart'; |
| |
| void main() { |
| testWidgets('ListView.builder mount/dismount smoke test', (WidgetTester tester) async { |
| final List<int> callbackTracker = <int>[]; |
| |
| // the root view is 800x600 in the test environment |
| // so if our widget is 100 pixels tall, it should fit exactly 6 times. |
| |
| Widget builder() { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: FlipWidget( |
| left: ListView.builder( |
| itemExtent: 100.0, |
| itemBuilder: (BuildContext context, int index) { |
| callbackTracker.add(index); |
| return SizedBox( |
| key: ValueKey<int>(index), |
| height: 100.0, |
| child: Text('$index'), |
| ); |
| }, |
| ), |
| right: const Text('Not Today'), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(builder()); |
| |
| final FlipWidgetState testWidget = tester.state(find.byType(FlipWidget)); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, 2, 3, 4, 5, // visible in viewport |
| 6, 7, 8, // in caching area |
| ])); |
| check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[ 6, 7, 8]); |
| |
| callbackTracker.clear(); |
| testWidget.flip(); |
| await tester.pump(); |
| |
| expect(callbackTracker, equals(<int>[])); |
| |
| callbackTracker.clear(); |
| testWidget.flip(); |
| await tester.pump(); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, 2, 3, 4, 5, |
| 6, 7, 8, // in caching area |
| ])); |
| check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[ 6, 7, 8]); |
| }); |
| |
| testWidgets('ListView.builder vertical', (WidgetTester tester) async { |
| final List<int> callbackTracker = <int>[]; |
| |
| // the root view is 800x600 in the test environment |
| // so if our widget is 200 pixels tall, it should fit exactly 3 times. |
| // but if we are offset by 300 pixels, there will be 4, numbered 1-4. |
| |
| Widget itemBuilder(BuildContext context, int index) { |
| callbackTracker.add(index); |
| return SizedBox( |
| key: ValueKey<int>(index), |
| width: 500.0, // this should be ignored |
| height: 400.0, // should be overridden by itemExtent |
| child: Text('$index', textDirection: TextDirection.ltr), |
| ); |
| } |
| |
| Widget buildWidget() { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: FlipWidget( |
| left: ListView.builder( |
| controller: ScrollController(initialScrollOffset: 300.0), |
| itemExtent: 200.0, |
| itemBuilder: itemBuilder, |
| ), |
| right: const Text('Not Today'), |
| ), |
| ); |
| } |
| |
| void jumpTo(double newScrollOffset) { |
| final ScrollableState scrollable = tester.state(find.byType(Scrollable)); |
| scrollable.position.jumpTo(newScrollOffset); |
| } |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, // in caching area |
| 1, 2, 3, 4, |
| 5, // in caching area |
| ])); |
| check(visible: <int>[1, 2, 3, 4], hidden: <int>[0, 5]); |
| callbackTracker.clear(); |
| |
| jumpTo(400.0); |
| // now only 3 should fit, numbered 2-4. |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, // in caching area |
| 2, 3, 4, |
| 5, 6, // in caching area |
| ])); |
| check(visible: <int>[2, 3, 4], hidden: <int>[0, 1, 5, 6]); |
| callbackTracker.clear(); |
| |
| jumpTo(500.0); |
| // now 4 should fit, numbered 2-5. |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, // in caching area |
| 2, 3, 4, 5, |
| 6, // in caching area |
| ])); |
| check(visible: <int>[2, 3, 4, 5], hidden: <int>[0, 1, 6]); |
| callbackTracker.clear(); |
| }); |
| |
| testWidgets('ListView.builder horizontal', (WidgetTester tester) async { |
| final List<int> callbackTracker = <int>[]; |
| |
| // the root view is 800x600 in the test environment |
| // so if our widget is 200 pixels wide, it should fit exactly 4 times. |
| // but if we are offset by 300 pixels, there will be 5, numbered 1-5. |
| |
| Widget itemBuilder(BuildContext context, int index) { |
| callbackTracker.add(index); |
| return SizedBox( |
| key: ValueKey<int>(index), |
| width: 400.0, // this should be overridden by itemExtent |
| height: 500.0, // this should be ignored |
| child: Text('$index'), |
| ); |
| } |
| |
| Widget buildWidget() { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: FlipWidget( |
| left: ListView.builder( |
| controller: ScrollController(initialScrollOffset: 300.0), |
| itemBuilder: itemBuilder, |
| itemExtent: 200.0, |
| scrollDirection: Axis.horizontal, |
| ), |
| right: const Text('Not Today'), |
| ), |
| ); |
| } |
| |
| void jumpTo(double newScrollOffset) { |
| final ScrollableState scrollable = tester.state(find.byType(Scrollable)); |
| scrollable.position.jumpTo(newScrollOffset); |
| } |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, // in caching area |
| 1, 2, 3, 4, 5, |
| 6, // in caching area |
| ])); |
| check(visible: <int>[1, 2, 3, 4, 5], hidden: <int>[0, 6]); |
| callbackTracker.clear(); |
| |
| jumpTo(400.0); |
| // now only 4 should fit, numbered 2-5. |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, // in caching area |
| 2, 3, 4, 5, |
| 6, 7, // in caching area |
| ])); |
| check(visible: <int>[2, 3, 4, 5], hidden: <int>[0, 1, 6, 7]); |
| callbackTracker.clear(); |
| |
| jumpTo(500.0); |
| // now only 5 should fit, numbered 2-6. |
| |
| await tester.pumpWidget(buildWidget()); |
| |
| expect(callbackTracker, equals(<int>[ |
| 0, 1, // in caching area |
| 2, 3, 4, 5, 6, |
| 7, // in caching area |
| ])); |
| check(visible: <int>[2, 3, 4, 5, 6], hidden: <int>[0, 1, 7]); |
| callbackTracker.clear(); |
| }); |
| |
| testWidgets('ListView.builder 10 items, 2-3 items visible', (WidgetTester tester) async { |
| final List<int> callbackTracker = <int>[]; |
| |
| // The root view is 800x600 in the test environment and our list |
| // items are 300 tall. Scrolling should cause two or three items |
| // to be built. |
| |
| Widget itemBuilder(BuildContext context, int index) { |
| callbackTracker.add(index); |
| return Text('$index', key: ValueKey<int>(index), textDirection: TextDirection.ltr); |
| } |
| |
| final Widget testWidget = Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView.builder( |
| itemBuilder: itemBuilder, |
| itemExtent: 300.0, |
| itemCount: 10, |
| ), |
| ); |
| |
| void jumpTo(double newScrollOffset) { |
| final ScrollableState scrollable = tester.state(find.byType(Scrollable)); |
| scrollable.position.jumpTo(newScrollOffset); |
| } |
| |
| await tester.pumpWidget(testWidget); |
| expect(callbackTracker, equals(<int>[0, 1, 2])); |
| check(visible: <int>[0, 1], hidden: <int>[2]); |
| callbackTracker.clear(); |
| |
| jumpTo(150.0); |
| await tester.pump(); |
| |
| expect(callbackTracker, equals(<int>[3])); |
| check(visible: <int>[0, 1, 2], hidden: <int>[3]); |
| callbackTracker.clear(); |
| |
| jumpTo(600.0); |
| await tester.pump(); |
| |
| expect(callbackTracker, equals(<int>[4])); |
| check(visible: <int>[2, 3], hidden: <int>[0, 1, 4]); |
| callbackTracker.clear(); |
| |
| jumpTo(750.0); |
| await tester.pump(); |
| |
| expect(callbackTracker, equals(<int>[5])); |
| check(visible: <int>[2, 3, 4], hidden: <int>[0, 1, 5]); |
| callbackTracker.clear(); |
| }); |
| |
| testWidgets('ListView.builder 30 items with big jump, using prototypeItem', (WidgetTester tester) async { |
| final List<int> callbackTracker = <int>[]; |
| |
| // The root view is 800x600 in the test environment and our list |
| // items are 300 tall. Scrolling should cause two or three items |
| // to be built. |
| |
| Widget itemBuilder(BuildContext context, int index) { |
| callbackTracker.add(index); |
| return Text('$index', key: ValueKey<int>(index), textDirection: TextDirection.ltr); |
| } |
| |
| final Widget testWidget = Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView.builder( |
| itemBuilder: itemBuilder, |
| prototypeItem: const SizedBox( |
| width: 800, |
| height: 300, |
| ), |
| itemCount: 30, |
| ), |
| ); |
| |
| void jumpTo(double newScrollOffset) { |
| final ScrollableState scrollable = tester.state(find.byType(Scrollable)); |
| scrollable.position.jumpTo(newScrollOffset); |
| } |
| |
| await tester.pumpWidget(testWidget); |
| |
| // 2 is in the cache area, but not visible. |
| expect(callbackTracker, equals(<int>[0, 1, 2])); |
| final List<int> initialExpectedHidden = List<int>.generate(28, (int i) => i + 2); |
| check(visible: <int>[0, 1], hidden: initialExpectedHidden); |
| callbackTracker.clear(); |
| |
| // Jump to the end of the ListView. |
| jumpTo(8400); |
| await tester.pump(); |
| |
| // 27 is in the cache area, but not visible. |
| expect(callbackTracker, equals(<int>[27, 28, 29])); |
| final List<int> finalExpectedHidden = List<int>.generate(28, (int i) => i); |
| check(visible: <int>[28, 29], hidden: finalExpectedHidden); |
| callbackTracker.clear(); |
| }); |
| |
| testWidgets('ListView.separated', (WidgetTester tester) async { |
| Widget buildFrame({ required int itemCount }) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView.separated( |
| itemCount: itemCount, |
| itemBuilder: (BuildContext context, int index) { |
| return SizedBox( |
| height: 100.0, |
| child: Text('i$index'), |
| ); |
| }, |
| separatorBuilder: (BuildContext context, int index) { |
| return SizedBox( |
| height: 10.0, |
| child: Text('s$index'), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(itemCount: 0)); |
| expect(find.text('i0'), findsNothing); |
| expect(find.text('s0'), findsNothing); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 1)); |
| expect(find.text('i0'), findsOneWidget); |
| expect(find.text('s0'), findsNothing); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 2)); |
| expect(find.text('i0'), findsOneWidget); |
| expect(find.text('s0'), findsOneWidget); |
| expect(find.text('i1'), findsOneWidget); |
| expect(find.text('s1'), findsNothing); |
| |
| // ListView's height is 600, so items i0-i5 and s0-s4 fit. |
| await tester.pumpWidget(buildFrame(itemCount: 25)); |
| for (final String s in <String>['i0', 's0', 'i1', 's1', 'i2', 's2', 'i3', 's3', 'i4', 's4', 'i5']) |
| expect(find.text(s), findsOneWidget); |
| expect(find.text('s5'), findsNothing); |
| expect(find.text('i6'), findsNothing); |
| }); |
| |
| |
| testWidgets('ListView.separated uses correct semanticChildCount', (WidgetTester tester) async { |
| Widget buildFrame({ required int itemCount}) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView.separated( |
| itemCount: itemCount, |
| itemBuilder: (BuildContext context, int index) { |
| return SizedBox( |
| height: 100.0, |
| child: Text('i$index'), |
| ); |
| }, |
| separatorBuilder: (BuildContext context, int index) { |
| return SizedBox( |
| height: 10.0, |
| child: Text('s$index'), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| Scrollable scrollable() { |
| return tester.widget<Scrollable>( |
| find.descendant( |
| of: find.byType(ListView), |
| matching: find.byType(Scrollable), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(itemCount: 0)); |
| expect(scrollable().semanticChildCount, 0); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 1)); |
| expect(scrollable().semanticChildCount, 1); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 2)); |
| expect(scrollable().semanticChildCount, 2); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 3)); |
| expect(scrollable().semanticChildCount, 3); |
| |
| await tester.pumpWidget(buildFrame(itemCount: 4)); |
| expect(scrollable().semanticChildCount, 4); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/72292 |
| testWidgets('ListView.builder and SingleChildScrollView can work well together', (WidgetTester tester) async { |
| Widget builder(int itemCount) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: SingleChildScrollView( |
| child: ListView.builder( |
| shrinkWrap: true, |
| itemExtent: 35, |
| itemCount: itemCount, |
| itemBuilder: (BuildContext context, int index) { |
| return const Text('I love Flutter.'); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(builder(1)); |
| // Trigger relayout and garbage collect. |
| await tester.pumpWidget(builder(2)); |
| }); |
| } |
| |
| void check({ List<int> visible = const <int>[], List<int> hidden = const <int>[] }) { |
| for (final int i in visible) { |
| expect(find.text('$i'), findsOneWidget); |
| } |
| for (final int i in hidden) { |
| expect(find.text('$i'), findsNothing); |
| } |
| } |