blob: 829d13ddc4c955b6d0a304d3dfc163b5fe618baf [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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import 'test_widgets.dart';
void main() {
testWidgets('ListView 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(
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
6, 7, 8, // in cached area
]));
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, // visible
6, 7, 8, // in cached area
]));
});
testWidgets('ListView 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: 200.0,
child: Text('$index', textDirection: TextDirection.ltr),
);
}
Widget builder() {
return Directionality(
textDirection: TextDirection.ltr,
child: FlipWidget(
left: ListView.builder(
controller: ScrollController(initialScrollOffset: 300.0),
itemBuilder: itemBuilder,
),
right: const Text('Not Today'),
),
);
}
await tester.pumpWidget(builder());
// 0 is built to find its height
expect(callbackTracker, equals(<int>[
0, 1, 2, 3, 4,
5, // in cached area
]));
callbackTracker.clear();
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
scrollable.position.jumpTo(600.0); // now only 3 should fit, numbered 3-5.
await tester.pumpWidget(builder());
// We build the visible children to find their new size.
expect(callbackTracker, equals(<int>[
0, 1, 2,
3, 4, 5, //visible
6, 7,
]));
callbackTracker.clear();
await tester.pumpWidget(builder());
// 0 isn't built because they're not visible.
expect(callbackTracker, equals(<int>[
1, 2,
3, 4, 5, // visible
6, 7,
]));
callbackTracker.clear();
});
testWidgets('ListView 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),
height: 500.0, // this should be ignored
width: 200.0,
child: Text('$index', textDirection: TextDirection.ltr),
);
}
Widget builder() {
return Directionality(
textDirection: TextDirection.ltr,
child: FlipWidget(
left: ListView.builder(
scrollDirection: Axis.horizontal,
controller: ScrollController(initialScrollOffset: 500.0),
itemBuilder: itemBuilder,
),
right: const Text('Not Today'),
),
);
}
await tester.pumpWidget(builder());
// 0 is built to find its width
expect(callbackTracker, equals(<int>[0, 1, 2, 3, 4, 5, 6, 7]));
callbackTracker.clear();
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
scrollable.position.jumpTo(600.0); // now only 4 should fit, numbered 2-5.
await tester.pumpWidget(builder());
// We build the visible children to find their new size.
expect(callbackTracker, equals(<int>[1, 2, 3, 4, 5, 6, 7, 8]));
callbackTracker.clear();
await tester.pumpWidget(builder());
// 0 isn't built because they're not visible.
expect(callbackTracker, equals(<int>[1, 2, 3, 4, 5, 6, 7, 8]));
callbackTracker.clear();
});
testWidgets('ListView reinvoke builders', (WidgetTester tester) async {
final List<int> callbackTracker = <int>[];
final List<String?> text = <String?>[];
Widget itemBuilder(BuildContext context, int index) {
callbackTracker.add(index);
return SizedBox(
key: ValueKey<int>(index),
width: 500.0, // this should be ignored
height: 220.0,
child: Text('$index', textDirection: TextDirection.ltr),
);
}
void collectText(Widget widget) {
if (widget is Text)
text.add(widget.data);
}
Widget builder() {
return Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
itemBuilder: itemBuilder,
),
);
}
await tester.pumpWidget(builder());
expect(callbackTracker, equals(<int>[
0, 1, 2,
3, // in cached area
]));
callbackTracker.clear();
tester.allWidgets.forEach(collectText);
expect(text, equals(<String>['0', '1', '2', '3']));
text.clear();
await tester.pumpWidget(builder());
expect(callbackTracker, equals(<int>[
0, 1, 2,
3, // in cached area
]));
callbackTracker.clear();
tester.allWidgets.forEach(collectText);
expect(text, equals(<String>['0', '1', '2', '3']));
text.clear();
});
testWidgets('ListView reinvoke builders', (WidgetTester tester) async {
late StateSetter setState;
ThemeData themeData = ThemeData.light();
Widget itemBuilder(BuildContext context, int index) {
return Container(
key: ValueKey<int>(index),
width: 500.0, // this should be ignored
height: 220.0,
color: Theme.of(context).primaryColor,
child: Text('$index', textDirection: TextDirection.ltr),
);
}
final Widget viewport = ListView.builder(
itemBuilder: itemBuilder,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Theme(data: themeData, child: viewport);
},
),
),
);
Container widget = tester.firstWidget(find.byType(Container));
expect(widget.color, equals(Colors.blue));
setState(() {
themeData = ThemeData(primarySwatch: Colors.green);
});
await tester.pump();
widget = tester.firstWidget(find.byType(Container));
expect(widget.color, equals(Colors.green));
});
testWidgets('ListView padding', (WidgetTester tester) async {
Widget itemBuilder(BuildContext context, int index) {
return Container(
key: ValueKey<int>(index),
width: 500.0, // this should be ignored
height: 220.0,
color: Colors.green[500],
child: Text('$index', textDirection: TextDirection.ltr),
);
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(7.0, 3.0, 5.0, 11.0),
itemBuilder: itemBuilder,
),
),
);
final RenderBox firstBox = tester.renderObject(find.text('0'));
final Offset upperLeft = firstBox.localToGlobal(Offset.zero);
expect(upperLeft, equals(const Offset(7.0, 3.0)));
expect(firstBox.size.width, equals(800.0 - 12.0));
});
testWidgets('ListView underflow extents', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView(
addAutomaticKeepAlives: false,
addSemanticIndexes: false,
children: <Widget>[
Container(height: 100.0),
Container(height: 100.0),
Container(height: 100.0),
],
),
),
);
final RenderSliverList list = tester.renderObject(find.byType(SliverList));
expect(list.indexOf(list.firstChild!), equals(0));
expect(list.indexOf(list.lastChild!), equals(2));
expect(list.childScrollOffset(list.firstChild!), equals(0.0));
expect(list.geometry!.scrollExtent, equals(300.0));
expect(list, hasAGoodToStringDeep);
expect(
list.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes(
'RenderSliverList#00000 relayoutBoundary=up1\n'
' │ needs compositing\n'
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: SliverConstraints(AxisDirection.down,\n'
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
' │ crossAxisDirection: AxisDirection.right,\n'
' │ viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0,\n'
' │ cacheOrigin: 0.0)\n'
' │ geometry: SliverGeometry(scrollExtent: 300.0, paintExtent: 300.0,\n'
' │ maxPaintExtent: 300.0, cacheExtent: 300.0)\n'
' │ currently live children: 0 to 2\n'
' │\n'
' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ needs compositing\n'
' │ │ parentData: index=0; layoutOffset=0.0 (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ layer: OffsetLayer#00000\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ │ repaints)\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n'
' │ │\n'
' │ └─child: RenderLimitedBox#00000\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, h=100.0)\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ maxWidth: 0.0\n'
' │ │ maxHeight: 0.0\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=100.0)\n'
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ needs compositing\n'
' │ │ parentData: index=1; layoutOffset=100.0 (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ layer: OffsetLayer#00000\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ │ repaints)\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n'
' │ │\n'
' │ └─child: RenderLimitedBox#00000\n'
' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, h=100.0)\n'
' │ │ size: Size(800.0, 100.0)\n'
' │ │ maxWidth: 0.0\n'
' │ │ maxHeight: 0.0\n'
' │ │\n'
' │ └─child: RenderConstrainedBox#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=100.0)\n'
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ needs compositing\n'
' │ parentData: index=2; layoutOffset=200.0 (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 100.0)\n'
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderConstrainedBox#00000 relayoutBoundary=up3\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, 0.0<=h<=Infinity)\n'
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n'
' │\n'
' └─child: RenderLimitedBox#00000\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=100.0)\n'
' │ size: Size(800.0, 100.0)\n'
' │ maxWidth: 0.0\n'
' │ maxHeight: 0.0\n'
' │\n'
' └─child: RenderConstrainedBox#00000\n'
' parentData: <none> (can use size)\n'
' constraints: BoxConstraints(w=800.0, h=100.0)\n'
' size: Size(800.0, 100.0)\n'
' additionalConstraints: BoxConstraints(biggest)\n',
),
);
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
expect(position.viewportDimension, equals(600.0));
expect(position.minScrollExtent, equals(0.0));
});
testWidgets('ListView should not paint hidden children', (WidgetTester tester) async {
const Text text = Text('test');
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 200.0,
child: ListView(
cacheExtent: 500.0,
controller: ScrollController(initialScrollOffset: 300.0),
children: const <Widget>[
SizedBox(height: 140.0, child: text),
SizedBox(height: 160.0, child: text),
SizedBox(height: 90.0, child: text),
SizedBox(height: 110.0, child: text),
SizedBox(height: 80.0, child: text),
SizedBox(height: 70.0, child: text),
],
),
),
),
),
);
final RenderSliverList list = tester.renderObject(find.byType(SliverList));
expect(list, paintsExactlyCountTimes(#drawParagraph, 2));
});
testWidgets('ListView should paint with offset', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SizedBox(
height: 500.0,
child: CustomScrollView(
controller: ScrollController(initialScrollOffset: 120.0),
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
),
SliverList(
delegate: ListView.builder(
itemExtent: 100.0,
itemCount: 100,
itemBuilder: (_, __) => const SizedBox(
height: 40.0,
child: Text('hey'),
),
).childrenDelegate,
),
],
),
),
),
),
);
final RenderObject renderObject = tester.renderObject(find.byType(Scrollable));
expect(renderObject, paintsExactlyCountTimes(#drawParagraph, 10));
});
testWidgets('ListView should paint with rtl', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: SizedBox(
height: 200.0,
child: ListView.builder(
padding: EdgeInsets.zero,
scrollDirection: Axis.horizontal,
itemExtent: 200.0,
itemCount: 10,
itemBuilder: (_, int i) => Container(
height: 200.0,
width: 200.0,
color: i.isEven ? Colors.black : Colors.red,
),
),
),
),
);
final RenderObject renderObject = tester.renderObject(find.byType(Scrollable));
expect(renderObject, paintsExactlyCountTimes(#drawRect, 4));
});
}