blob: 52b095735e0f52c014b3e7ab00d452f834f094c9 [file] [log] [blame]
// 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);
}
}