| // 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'; |
| |
| ScrollController _controller = ScrollController( |
| initialScrollOffset: 110.0, |
| ); |
| |
| class ThePositiveNumbers extends StatelessWidget { |
| const ThePositiveNumbers({ |
| Key? key, |
| required this.from, |
| }) : super(key: key); |
| final int from; |
| @override |
| Widget build(BuildContext context) { |
| return ListView.builder( |
| key: const PageStorageKey<String>('ThePositiveNumbers'), |
| itemExtent: 100.0, |
| controller: _controller, |
| itemBuilder: (BuildContext context, int index) { |
| return Text('${index + from}', key: ValueKey<int>(index)); |
| }, |
| ); |
| } |
| } |
| |
| Future<void> performTest(WidgetTester tester, bool maintainState) async { |
| final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Navigator( |
| key: navigatorKey, |
| onGenerateRoute: (RouteSettings settings) { |
| if (settings.name == '/') { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (_) => const ThePositiveNumbers(from: 0), |
| maintainState: maintainState, |
| ); |
| } else if (settings.name == '/second') { |
| return MaterialPageRoute<void>( |
| settings: settings, |
| builder: (_) => const ThePositiveNumbers(from: 10000), |
| maintainState: maintainState, |
| ); |
| } |
| return null; |
| }, |
| ), |
| ), |
| ); |
| |
| // we're 600 pixels high, each item is 100 pixels high, scroll position is |
| // 110.0, so we should have 7 items, 1..7. |
| expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('1'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('2'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('3'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('4'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('5'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('6'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('7'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState'); |
| |
| tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(1000.0); |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // we're 600 pixels high, each item is 100 pixels high, scroll position is |
| // 1000, so we should have exactly 6 items, 10..15. |
| |
| expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('9'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('11'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('12'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('13'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('14'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('15'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('16'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState'); |
| |
| navigatorKey.currentState!.pushNamed('/second'); |
| await tester.pump(); // navigating always takes two frames, one to start... |
| await tester.pump(const Duration(seconds: 1)); // ...and one to end the transition |
| |
| // the second list is now visible, starting at 10001 |
| expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('11'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10000'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10001'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10002'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10003'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10004'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10005'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10006'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10007'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10008'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10010'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10100'), findsNothing, reason: 'with maintainState: $maintainState'); |
| |
| navigatorKey.currentState!.pop(); |
| await tester.pump(); // again, navigating always takes two frames |
| |
| // Ensure we don't clamp the scroll offset even during the navigation. |
| // https://github.com/flutter/flutter/issues/4883 |
| final ScrollableState state = tester.state(find.byType(Scrollable).first); |
| expect(state.position.pixels, equals(1000.0), reason: 'with maintainState: $maintainState'); |
| |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // we're 600 pixels high, each item is 100 pixels high, scroll position is |
| // 1000, so we should have exactly 6 items, 10..15. |
| |
| expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('9'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('10'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('11'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('12'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('13'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('14'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('15'), findsOneWidget, reason: 'with maintainState: $maintainState'); |
| expect(find.text('16'), findsNothing, reason: 'with maintainState: $maintainState'); |
| expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState'); |
| } |
| |
| void main() { |
| testWidgets("ScrollPosition jumpTo() doesn't call notifyListeners twice", (WidgetTester tester) async { |
| int count = 0; |
| await tester.pumpWidget(MaterialApp( |
| home: ListView.builder( |
| itemBuilder: (BuildContext context, int index) { |
| return Text('$index', textDirection: TextDirection.ltr); |
| }, |
| ), |
| )); |
| |
| final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position; |
| position.addListener(() { |
| count++; |
| }); |
| position.jumpTo(100); |
| |
| expect(count, 1); |
| }); |
| |
| testWidgets('whether we remember our scroll position', (WidgetTester tester) async { |
| await performTest(tester, true); |
| await performTest(tester, false); |
| }); |
| |
| testWidgets('scroll alignment is honored by ensureVisible', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(11, (int index) => index).toList(); |
| final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: ListView( |
| controller: controller, |
| children: items.map<Widget>((int item) { |
| return Focus( |
| key: ValueKey<int>(item), |
| focusNode: nodes[item], |
| child: Container(height: 110), |
| ); |
| }).toList(), |
| ), |
| ), |
| ); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(0))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(1))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(1))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(4))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(5))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(5))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
| ); |
| expect(controller.position.pixels, equals(60.0)); |
| |
| controller.position.ensureVisible( |
| tester.renderObject(find.byKey(const ValueKey<int>(0))), |
| alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
| ); |
| expect(controller.position.pixels, equals(0.0)); |
| }); |
| |
| testWidgets('jumpTo recommends deferred loading', (WidgetTester tester) async { |
| int loadedWithDeferral = 0; |
| int buildCount = 0; |
| const double height = 500; |
| await tester.pumpWidget(MaterialApp( |
| home: ListView.builder( |
| itemBuilder: (BuildContext context, int index) { |
| buildCount += 1; |
| if (Scrollable.recommendDeferredLoadingForContext(context)) { |
| loadedWithDeferral += 1; |
| } |
| return const SizedBox(height: height); |
| }, |
| ), |
| )); |
| |
| // The two visible on screen should have loaded without deferral. |
| expect(buildCount, 2); |
| expect(loadedWithDeferral, 0); |
| |
| final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position; |
| position.jumpTo(height * 100); |
| await tester.pump(); |
| |
| // All but the first two that were loaded normally should have gotten a |
| // recommendation to defer. |
| expect(buildCount, 102); |
| expect(loadedWithDeferral, 100); |
| |
| position.jumpTo(height * 102); |
| await tester.pump(); |
| |
| // The smaller jump should not have recommended deferral. |
| expect(buildCount, 104); |
| expect(loadedWithDeferral, 100); |
| }); |
| } |