| // 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_test/flutter_test.dart'; |
| |
| void main() { |
| testWidgets('SliverList reverse children (with keys)', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(20, (int i) => i); |
| const double itemHeight = 300.0; |
| const double viewportHeight = 500.0; |
| |
| const double scrollPosition = 18 * itemHeight; |
| final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items, |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.offset, scrollPosition); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items.reversed.toList(), |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| final int frames = await tester.pumpAndSettle(); |
| expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable |
| |
| expect(controller.offset, scrollPosition); |
| expect(find.text('Tile 19'), findsNothing); |
| expect(find.text('Tile 18'), findsNothing); |
| expect(find.text('Tile 1'), findsOneWidget); |
| expect(find.text('Tile 0'), findsOneWidget); |
| |
| controller.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.offset, 0.0); |
| expect(find.text('Tile 19'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 0'), findsNothing); |
| }); |
| |
| testWidgets('SliverList replace children (with keys)', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(20, (int i) => i); |
| const double itemHeight = 300.0; |
| const double viewportHeight = 500.0; |
| |
| const double scrollPosition = 18 * itemHeight; |
| final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items, |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.offset, scrollPosition); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items.map<int>((int i) => i + 100).toList(), |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| final int frames = await tester.pumpAndSettle(); |
| expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable |
| |
| expect(controller.offset, scrollPosition); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 18'), findsNothing); |
| expect(find.text('Tile 19'), findsNothing); |
| |
| expect(find.text('Tile 100'), findsNothing); |
| expect(find.text('Tile 101'), findsNothing); |
| expect(find.text('Tile 118'), findsOneWidget); |
| expect(find.text('Tile 119'), findsOneWidget); |
| |
| controller.jumpTo(0.0); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.offset, 0.0); |
| expect(find.text('Tile 100'), findsOneWidget); |
| expect(find.text('Tile 101'), findsOneWidget); |
| expect(find.text('Tile 118'), findsNothing); |
| expect(find.text('Tile 119'), findsNothing); |
| }); |
| |
| testWidgets('SliverList replace with shorter children list (with keys)', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(20, (int i) => i); |
| const double itemHeight = 300.0; |
| const double viewportHeight = 500.0; |
| |
| final double scrollPosition = items.length * itemHeight - viewportHeight; |
| final ScrollController controller = ScrollController(initialScrollOffset: scrollPosition); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items, |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| await tester.pumpAndSettle(); |
| |
| expect(controller.offset, scrollPosition); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 17'), findsNothing); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| |
| await tester.pumpWidget(_buildSliverList( |
| items: items.sublist(0, items.length - 1), |
| controller: controller, |
| itemHeight: itemHeight, |
| viewportHeight: viewportHeight, |
| )); |
| final int frames = await tester.pumpAndSettle(); |
| expect(frames, 1); // No animation when content shrinks suddenly. |
| |
| expect(controller.offset, scrollPosition - itemHeight); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 1'), findsNothing); |
| expect(find.text('Tile 17'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsNothing); |
| }); |
| |
| testWidgets('SliverList should layout first child in case of child reordering', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/35904. |
| List<String> items = <String>['1', '2']; |
| |
| await tester.pumpWidget(_buildSliverListRenderWidgetChild(items)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Tile 1'), findsOneWidget); |
| expect(find.text('Tile 2'), findsOneWidget); |
| |
| items = items.reversed.toList(); |
| await tester.pumpWidget(_buildSliverListRenderWidgetChild(items)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Tile 1'), findsOneWidget); |
| expect(find.text('Tile 2'), findsOneWidget); |
| }); |
| |
| testWidgets('SliverList should recalculate inaccurate layout offset case 1', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/42142. |
| final List<int> items = List<int>.generate(20, (int i) => i); |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget( |
| _buildSliverList( |
| items: List<int>.from(items), |
| controller: controller, |
| itemHeight: 50, |
| viewportHeight: 200, |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0)); |
| await tester.pumpAndSettle(); |
| |
| // Viewport should be scrolled to the end of list. |
| expect(controller.offset, 800.0); |
| expect(find.text('Tile 15'), findsNothing); |
| expect(find.text('Tile 16'), findsOneWidget); |
| expect(find.text('Tile 17'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| |
| // Prepends item to the list. |
| items.insert(0, -1); |
| await tester.pumpWidget( |
| _buildSliverList( |
| items: List<int>.from(items), |
| controller: controller, |
| itemHeight: 50, |
| viewportHeight: 200, |
| ), |
| ); |
| await tester.pump(); |
| // We need second pump to ensure the scheduled animation gets run. |
| await tester.pumpAndSettle(); |
| // Scroll offset should stay the same, and the items in viewport should be |
| // shifted by one. |
| expect(controller.offset, 800.0); |
| expect(find.text('Tile 14'), findsNothing); |
| expect(find.text('Tile 15'), findsOneWidget); |
| expect(find.text('Tile 16'), findsOneWidget); |
| expect(find.text('Tile 17'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsNothing); |
| |
| // Drags back to beginning and newly added item is visible. |
| await tester.drag(find.text('Tile 16'), const Offset(0.0, 1000.0)); |
| await tester.pumpAndSettle(); |
| expect(controller.offset, 0.0); |
| expect(find.text('Tile -1'), findsOneWidget); |
| expect(find.text('Tile 0'), findsOneWidget); |
| expect(find.text('Tile 1'), findsOneWidget); |
| expect(find.text('Tile 2'), findsOneWidget); |
| expect(find.text('Tile 3'), findsNothing); |
| |
| }); |
| |
| testWidgets('SliverList should recalculate inaccurate layout offset case 2', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/42142. |
| final List<int> items = List<int>.generate(20, (int i) => i); |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget( |
| _buildSliverList( |
| items: List<int>.from(items), |
| controller: controller, |
| itemHeight: 50, |
| viewportHeight: 200, |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0)); |
| await tester.pumpAndSettle(); |
| |
| // Viewport should be scrolled to the end of list. |
| expect(controller.offset, 800.0); |
| expect(find.text('Tile 15'), findsNothing); |
| expect(find.text('Tile 16'), findsOneWidget); |
| expect(find.text('Tile 17'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| |
| // Reorders item to the front. This should make item 19 to be first child |
| // with layout offset = null. |
| final int swap = items[19]; |
| items[19] = items[3]; |
| items[3] = swap; |
| |
| await tester.pumpWidget( |
| _buildSliverList( |
| items: List<int>.from(items), |
| controller: controller, |
| itemHeight: 50, |
| viewportHeight: 200, |
| ), |
| ); |
| await tester.pump(); |
| // We need second pump to ensure the scheduled animation gets run. |
| await tester.pumpAndSettle(); |
| // Scroll offset should stay the same |
| expect(controller.offset, 800.0); |
| expect(find.text('Tile 14'), findsNothing); |
| expect(find.text('Tile 15'), findsNothing); |
| expect(find.text('Tile 16'), findsOneWidget); |
| expect(find.text('Tile 17'), findsOneWidget); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 3'), findsOneWidget); |
| }); |
| |
| testWidgets('SliverList should start to perform layout from the initial child when there is no valid offset', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/66198. |
| bool isShow = true; |
| final ScrollController controller = ScrollController(); |
| Widget buildSliverList(ScrollController controller) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: SizedBox( |
| height: 200, |
| child: ListView( |
| controller: controller, |
| children: <Widget>[ |
| if (isShow) |
| for (int i = 0; i < 20; i++) |
| SizedBox( |
| height: 50, |
| child: Text('Tile $i'), |
| ), |
| const SizedBox(), // Use this widget to occupy the position where the offset is 0 when rebuild |
| const SizedBox(key: Key('key0'), height: 50.0), |
| const SizedBox(key: Key('key1'), height: 50.0), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildSliverList(controller)); |
| await tester.pumpAndSettle(); |
| |
| // Scrolling to the bottom. |
| await tester.drag(find.text('Tile 2'), const Offset(0.0, -1000.0)); |
| await tester.pumpAndSettle(); |
| |
| // Viewport should be scrolled to the end of list. |
| expect(controller.offset, 900.0); |
| expect(find.text('Tile 17'), findsNothing); |
| expect(find.text('Tile 18'), findsOneWidget); |
| expect(find.text('Tile 19'), findsOneWidget); |
| expect(find.byKey(const Key('key0')), findsOneWidget); |
| expect(find.byKey(const Key('key1')), findsOneWidget); |
| |
| // Trigger rebuild. |
| isShow = false; |
| await tester.pumpWidget(buildSliverList(controller)); |
| |
| // After rebuild, [ContainerRenderObjectMixin] has two children, and |
| // neither of them has a valid layout offset. |
| // SliverList can layout normally without any assert or dead loop. |
| // Only the 'SizeBox' show in the viewport. |
| expect(controller.offset, 0.0); |
| expect(find.text('Tile 0'), findsNothing); |
| expect(find.text('Tile 19'), findsNothing); |
| expect(find.byKey(const Key('key0')), findsOneWidget); |
| expect(find.byKey(const Key('key1')), findsOneWidget); |
| }); |
| } |
| |
| Widget _buildSliverListRenderWidgetChild(List<String> items) { |
| return MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: SizedBox( |
| height: 500, |
| child: CustomScrollView( |
| controller: ScrollController(), |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildListDelegate( |
| items.map<Widget>((String item) { |
| return Chip( |
| key: Key(item), |
| label: Text('Tile $item'), |
| ); |
| }).toList(), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Widget _buildSliverList({ |
| List<int> items = const <int>[], |
| ScrollController? controller, |
| double itemHeight = 500.0, |
| double viewportHeight = 300.0, |
| }) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: SizedBox( |
| height: viewportHeight, |
| child: CustomScrollView( |
| controller: controller, |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int i) { |
| return SizedBox( |
| key: ValueKey<int>(items[i]), |
| height: itemHeight, |
| child: Text('Tile ${items[i]}'), |
| ); |
| }, |
| findChildIndexCallback: (Key key) { |
| final ValueKey<int> valueKey = key as ValueKey<int>; |
| final int index = items.indexOf(valueKey.value); |
| return index == -1 ? null : index; |
| }, |
| childCount: items.length, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |