// 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({ super.key, required this.from, }); 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: MediaQuery( data: MediaQueryData.fromView(tester.view), 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); }); }