| // 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)); |
| }); |
| } |