// 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/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; late GestureVelocityTrackerBuilder lastCreatedBuilder; class TestScrollBehavior extends ScrollBehavior { const TestScrollBehavior(this.flag); final bool flag; @override ScrollPhysics getScrollPhysics(BuildContext context) { return flag ? const ClampingScrollPhysics() : const BouncingScrollPhysics(); } @override bool shouldNotify(TestScrollBehavior old) => flag != old.flag; @override GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) { lastCreatedBuilder = flag ? (PointerEvent ev) => VelocityTracker.withKind(ev.kind) : (PointerEvent ev) => IOSScrollViewFlingVelocityTracker(ev.kind); return lastCreatedBuilder; } } void main() { // Regression test for https://github.com/flutter/flutter/issues/89681 testWidgets('_WrappedScrollBehavior shouldNotify test', (WidgetTester tester) async { final ScrollBehavior behavior1 = const ScrollBehavior().copyWith(); final ScrollBehavior behavior2 = const ScrollBehavior().copyWith(); expect(behavior1.shouldNotify(behavior2), false); }); testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async { final GlobalKey key = GlobalKey(debugLabel: 'scrollable'); TestScrollBehavior? behavior; late ScrollPositionWithSingleContext position; final Widget scrollView = SingleChildScrollView( key: key, child: Builder( builder: (BuildContext context) { behavior = ScrollConfiguration.of(context) as TestScrollBehavior; position = Scrollable.of(context)!.position as ScrollPositionWithSingleContext; return Container(height: 1000.0); }, ), ); await tester.pumpWidget( ScrollConfiguration( behavior: const TestScrollBehavior(true), child: scrollView, ), ); expect(behavior, isNotNull); expect(behavior!.flag, isTrue); expect(position.physics, isA<ClampingScrollPhysics>()); expect(lastCreatedBuilder(const PointerDownEvent()), isA<VelocityTracker>()); ScrollMetrics metrics = position.copyWith(); expect(metrics.extentAfter, equals(400.0)); expect(metrics.viewportDimension, equals(600.0)); // Same Scrollable, different ScrollConfiguration await tester.pumpWidget( ScrollConfiguration( behavior: const TestScrollBehavior(false), child: scrollView, ), ); expect(behavior, isNotNull); expect(behavior!.flag, isFalse); expect(position.physics, isA<BouncingScrollPhysics>()); expect(lastCreatedBuilder(const PointerDownEvent()), isA<IOSScrollViewFlingVelocityTracker>()); // Regression test for https://github.com/flutter/flutter/issues/5856 metrics = position.copyWith(); expect(metrics.extentAfter, equals(400.0)); expect(metrics.viewportDimension, equals(600.0)); }); testWidgets('ScrollBehavior default android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: ScrollConfiguration( behavior: const ScrollBehavior(), child: ListView( children: const <Widget>[ SizedBox( height: 1000.0, width: 1000.0, child: Text('Test'), ), ], ), ), ), ); expect(find.byType(StretchingOverscrollIndicator), findsNothing); expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); testWidgets('ScrollBehavior stretch android overscroll indicator', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800, 600)), child: ScrollConfiguration( behavior: const ScrollBehavior(androidOverscrollIndicator: AndroidOverscrollIndicator.stretch), child: ListView( children: const <Widget>[ SizedBox( height: 1000.0, width: 1000.0, child: Text('Test'), ), ], ), ), ), ), ); expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); expect(find.byType(GlowingOverscrollIndicator), findsNothing); }, variant: TargetPlatformVariant.only(TargetPlatform.android)); group('ScrollBehavior configuration is maintained over multiple copies', () { testWidgets('dragDevices', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); expect(defaultBehavior.dragDevices, <PointerDeviceKind>{ PointerDeviceKind.touch, PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, PointerDeviceKind.trackpad, PointerDeviceKind.unknown, }); // Use copyWith to modify drag devices final ScrollBehavior onceCopiedBehavior = defaultBehavior.copyWith( dragDevices: PointerDeviceKind.values.toSet(), ); expect(onceCopiedBehavior.dragDevices, PointerDeviceKind.values.toSet()); // Copy again. The previously modified drag devices should carry over. final ScrollBehavior twiceCopiedBehavior = onceCopiedBehavior.copyWith(); expect(twiceCopiedBehavior.dragDevices, PointerDeviceKind.values.toSet()); }); testWidgets('androidOverscrollIndicator', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); expect(defaultBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.glow); // Use copyWith to modify androidOverscrollIndicator final ScrollBehavior onceCopiedBehavior = defaultBehavior.copyWith( androidOverscrollIndicator: AndroidOverscrollIndicator.stretch, ); expect(onceCopiedBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.stretch); // Copy again. The previously modified value should carry over. final ScrollBehavior twiceCopiedBehavior = onceCopiedBehavior.copyWith(); expect(twiceCopiedBehavior.androidOverscrollIndicator, AndroidOverscrollIndicator.stretch); }); testWidgets('physics', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 late ScrollPhysics defaultPhysics; late ScrollPhysics onceCopiedPhysics; late ScrollPhysics twiceCopiedPhysics; await tester.pumpWidget(ScrollConfiguration( // Default ScrollBehavior behavior: const ScrollBehavior(), child: Builder( builder: (BuildContext context) { final ScrollBehavior defaultBehavior = ScrollConfiguration.of(context); // Copy once to change physics defaultPhysics = defaultBehavior.getScrollPhysics(context); return ScrollConfiguration( behavior: defaultBehavior.copyWith(physics: const BouncingScrollPhysics()), child: Builder( builder: (BuildContext context) { final ScrollBehavior onceCopiedBehavior = ScrollConfiguration.of(context); onceCopiedPhysics = onceCopiedBehavior.getScrollPhysics(context); return ScrollConfiguration( // Copy again, physics should follow behavior: onceCopiedBehavior.copyWith(), child: Builder( builder: (BuildContext context) { twiceCopiedPhysics = ScrollConfiguration.of(context).getScrollPhysics(context); return SingleChildScrollView(child: Container(height: 1000)); } ) ); } ) ); } ), )); expect(defaultPhysics, const ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics())); expect(onceCopiedPhysics, const BouncingScrollPhysics()); expect(twiceCopiedPhysics, const BouncingScrollPhysics()); }); testWidgets('platform', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 late TargetPlatform defaultPlatform; late TargetPlatform onceCopiedPlatform; late TargetPlatform twiceCopiedPlatform; await tester.pumpWidget(ScrollConfiguration( // Default ScrollBehavior behavior: const ScrollBehavior(), child: Builder( builder: (BuildContext context) { final ScrollBehavior defaultBehavior = ScrollConfiguration.of(context); // Copy once to change physics defaultPlatform = defaultBehavior.getPlatform(context); return ScrollConfiguration( behavior: defaultBehavior.copyWith(platform: TargetPlatform.fuchsia), child: Builder( builder: (BuildContext context) { final ScrollBehavior onceCopiedBehavior = ScrollConfiguration.of(context); onceCopiedPlatform = onceCopiedBehavior.getPlatform(context); return ScrollConfiguration( // Copy again, physics should follow behavior: onceCopiedBehavior.copyWith(), child: Builder( builder: (BuildContext context) { twiceCopiedPlatform = ScrollConfiguration.of(context).getPlatform(context); return SingleChildScrollView(child: Container(height: 1000)); } ) ); } ) ); } ), )); expect(defaultPlatform, TargetPlatform.android); expect(onceCopiedPlatform, TargetPlatform.fuchsia); expect(twiceCopiedPlatform, TargetPlatform.fuchsia); }); Widget wrap(ScrollBehavior behavior) { return Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(500, 500)), child: ScrollConfiguration( behavior: behavior, child: Builder( builder: (BuildContext context) => SingleChildScrollView(child: Container(height: 1000)) ) ), ) ); } testWidgets('scrollbar', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); await tester.pumpWidget(wrap(defaultBehavior)); // Default adds a scrollbar expect(find.byType(RawScrollbar), findsOneWidget); final ScrollBehavior onceCopiedBehavior = defaultBehavior.copyWith(scrollbars: false); await tester.pumpWidget(wrap(onceCopiedBehavior)); // Copy does not add scrollbar expect(find.byType(RawScrollbar), findsNothing); final ScrollBehavior twiceCopiedBehavior = onceCopiedBehavior.copyWith(); await tester.pumpWidget(wrap(twiceCopiedBehavior)); // Second copy maintains scrollbar setting expect(find.byType(RawScrollbar), findsNothing); // For default scrollbars }, variant: TargetPlatformVariant.desktop()); testWidgets('overscroll', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/91673 const ScrollBehavior defaultBehavior = ScrollBehavior(); await tester.pumpWidget(wrap(defaultBehavior)); // Default adds a glowing overscroll indicator expect(find.byType(GlowingOverscrollIndicator), findsOneWidget); final ScrollBehavior onceCopiedBehavior = defaultBehavior.copyWith(overscroll: false); await tester.pumpWidget(wrap(onceCopiedBehavior)); // Copy does not add indicator expect(find.byType(GlowingOverscrollIndicator), findsNothing); final ScrollBehavior twiceCopiedBehavior = onceCopiedBehavior.copyWith(); await tester.pumpWidget(wrap(twiceCopiedBehavior)); // Second copy maintains overscroll setting expect(find.byType(GlowingOverscrollIndicator), findsNothing); // For default glowing indicator }, variant: TargetPlatformVariant.only(TargetPlatform.android)); }); }