// 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/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; class ScrollPositionListener extends StatefulWidget { const ScrollPositionListener({ super.key, required this.child, required this.log}); final Widget child; final ValueChanged<String> log; @override State<ScrollPositionListener> createState() => _ScrollPositionListenerState(); } class _ScrollPositionListenerState extends State<ScrollPositionListener> { ScrollPosition? _position; @override void didChangeDependencies() { super.didChangeDependencies(); _position?.removeListener(listener); _position = Scrollable.maybeOf(context)?.position; _position?.addListener(listener); widget.log('didChangeDependencies ${_position?.pixels.toStringAsFixed(1)}'); } @override void dispose() { _position?.removeListener(listener); super.dispose(); } @override Widget build(BuildContext context) => widget.child; void listener() { widget.log('listener ${_position?.pixels.toStringAsFixed(1)}'); } } class TestScrollController extends ScrollController { TestScrollController({ required this.deferLoading }); final bool deferLoading; @override ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { return TestScrollPosition( physics: physics, context: context, oldPosition: oldPosition, deferLoading: deferLoading, ); } } class TestScrollPosition extends ScrollPositionWithSingleContext { TestScrollPosition({ required super.physics, required super.context, super.oldPosition, required this.deferLoading, }); final bool deferLoading; @override bool recommendDeferredLoading(BuildContext context) => deferLoading; } class TestScrollable extends StatefulWidget { const TestScrollable({ super.key, required this.child }); final Widget child; @override State<StatefulWidget> createState() => TestScrollableState(); } class TestScrollableState extends State<TestScrollable> { int dependenciesChanged = 0; @override void didChangeDependencies() { dependenciesChanged += 1; super.didChangeDependencies(); } @override Widget build(BuildContext context) { return widget.child; } } class TestChild extends StatefulWidget { const TestChild({ super.key }); @override State<TestChild> createState() => TestChildState(); } class TestChildState extends State<TestChild> { int dependenciesChanged = 0; late ScrollableState scrollable; @override void didChangeDependencies() { dependenciesChanged += 1; scrollable = Scrollable.of(context, axis: Axis.horizontal); super.didChangeDependencies(); } @override Widget build(BuildContext context) { return SizedBox.square( dimension: 1000, child: Text(scrollable.axisDirection.toString()), ); } } void main() { testWidgets('Scrollable.of() dependent rebuilds when Scrollable position changes', (WidgetTester tester) async { late String logValue; final ScrollController controller = ScrollController(); addTearDown(controller.dispose); // Changing the SingleChildScrollView's physics causes the // ScrollController's ScrollPosition to be rebuilt. Widget buildFrame(ScrollPhysics? physics) { return SingleChildScrollView( controller: controller, physics: physics, child: ScrollPositionListener( log: (String s) { logValue = s; }, child: const SizedBox(height: 400.0), ), ); } await tester.pumpWidget(buildFrame(null)); expect(logValue, 'didChangeDependencies 0.0'); controller.jumpTo(100.0); expect(logValue, 'listener 100.0'); await tester.pumpWidget(buildFrame(const ClampingScrollPhysics())); expect(logValue, 'didChangeDependencies 100.0'); controller.jumpTo(200.0); expect(logValue, 'listener 200.0'); controller.jumpTo(300.0); expect(logValue, 'listener 300.0'); await tester.pumpWidget(buildFrame(const BouncingScrollPhysics())); expect(logValue, 'didChangeDependencies 300.0'); controller.jumpTo(400.0); expect(logValue, 'listener 400.0'); }); testWidgets('Scrollable.of() is possible using ScrollNotification context', (WidgetTester tester) async { late ScrollNotification notification; await tester.pumpWidget(NotificationListener<ScrollNotification>( onNotification: (ScrollNotification value) { notification = value; return false; }, child: const SingleChildScrollView( child: SizedBox(height: 1200.0), ), )); final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); await tester.pump(const Duration(seconds: 1)); final StatefulElement scrollableElement = find.byType(Scrollable).evaluate().first as StatefulElement; expect(Scrollable.of(notification.context!), equals(scrollableElement.state)); // Finish gesture to release resources. await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Static Scrollable methods can target a specific axis', (WidgetTester tester) async { final TestScrollController horizontalController = TestScrollController(deferLoading: true); addTearDown(horizontalController.dispose); final TestScrollController verticalController = TestScrollController(deferLoading: false); addTearDown(verticalController.dispose); late final AxisDirection foundAxisDirection; late final bool foundRecommendation; await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: horizontalController, child: SingleChildScrollView( controller: verticalController, child: Builder( builder: (BuildContext context) { foundAxisDirection = Scrollable.of( context, axis: Axis.horizontal, ).axisDirection; foundRecommendation = Scrollable.recommendDeferredLoadingForContext( context, axis: Axis.horizontal, ); return const SizedBox(height: 1200.0, width: 1200.0); } ), ), ), )); await tester.pumpAndSettle(); expect(foundAxisDirection, AxisDirection.right); expect(foundRecommendation, isTrue); }); testWidgets('Axis targeting scrollables establishes the correct dependencies', (WidgetTester tester) async { final GlobalKey<TestScrollableState> verticalKey = GlobalKey<TestScrollableState>(); final GlobalKey<TestChildState> childKey = GlobalKey<TestChildState>(); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: TestScrollable( key: verticalKey, child: TestChild(key: childKey), ), ), )); await tester.pumpAndSettle(); expect(verticalKey.currentState!.dependenciesChanged, 1); expect(childKey.currentState!.dependenciesChanged, 1); final ScrollController controller = ScrollController(); addTearDown(controller.dispose); // Change the horizontal ScrollView, adding a controller await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: controller, child: TestScrollable( key: verticalKey, child: TestChild(key: childKey), ), ), )); await tester.pumpAndSettle(); expect(verticalKey.currentState!.dependenciesChanged, 1); expect(childKey.currentState!.dependenciesChanged, 2); }); }