flutter_flutter/packages/flutter/test/widgets/scroll_behavior_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

546 lines
19 KiB
Dart

// 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/foundation.dart';
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() {
testWidgets(
'Assert in buildScrollbar that controller != null when using it',
(WidgetTester tester) async {
const defaultBehavior = ScrollBehavior();
late BuildContext capturedContext;
await tester.pumpWidget(
ScrollConfiguration(
// Avoid the default ones here.
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: SingleChildScrollView(
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return Container(height: 1000.0);
},
),
),
),
);
const details = ScrollableDetails(direction: AxisDirection.down);
final Widget child = Container();
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
// Does not throw if we aren't using it.
defaultBehavior.buildScrollbar(capturedContext, child, details);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
expect(
() {
defaultBehavior.buildScrollbar(capturedContext, child, details);
},
throwsA(
isA<AssertionError>().having(
(AssertionError error) => error.toString(),
'description',
contains('details.controller != null'),
),
),
);
}
},
variant: TargetPlatformVariant.all(),
);
// 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 multitouchDragStrategy test - 1', (WidgetTester tester) async {
const behavior1 = ScrollBehavior();
final ScrollBehavior behavior2 = const ScrollBehavior().copyWith(
multitouchDragStrategy: MultitouchDragStrategy.sumAllPointers,
);
final controller = ScrollController();
addTearDown(() => controller.dispose());
Widget buildFrame(ScrollBehavior behavior) {
return Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: behavior,
child: ListView(
controller: controller,
children: const <Widget>[
SizedBox(height: 1000.0, width: 1000.0, child: Text('I Love Flutter!')),
],
),
),
);
}
await tester.pumpWidget(buildFrame(behavior1));
expect(controller.position.pixels, 0.0);
final Offset listLocation = tester.getCenter(find.byType(ListView));
final TestGesture gesture1 = await tester.createGesture(pointer: 1);
await gesture1.down(listLocation);
await tester.pump();
final TestGesture gesture2 = await tester.createGesture(pointer: 2);
await gesture2.down(listLocation);
await tester.pump();
await gesture1.moveBy(const Offset(0, -50));
await tester.pump();
await gesture2.moveBy(const Offset(0, -50));
await tester.pump();
// The default multitouchDragStrategy is 'latestPointer' or 'averageBoundaryPointers,
// the received delta should be 50.0.
expect(controller.position.pixels, 50.0);
// Change to sumAllPointers.
await tester.pumpWidget(buildFrame(behavior2));
await gesture1.moveBy(const Offset(0, -50));
await tester.pump();
await gesture2.moveBy(const Offset(0, -50));
await tester.pump();
// All active pointers be tracked.
expect(controller.position.pixels, 50.0 + 50.0 + 50.0);
}, variant: TargetPlatformVariant.all());
testWidgets(
'ScrollBehavior multitouchDragStrategy test (non-Apple platforms) - 2',
(WidgetTester tester) async {
const behavior1 = ScrollBehavior();
final ScrollBehavior behavior2 = const ScrollBehavior().copyWith(
multitouchDragStrategy: MultitouchDragStrategy.averageBoundaryPointers,
);
final controller = ScrollController();
late BuildContext capturedContext;
addTearDown(() => controller.dispose());
Widget buildFrame(ScrollBehavior behavior) {
return Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: behavior,
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return ListView(
controller: controller,
children: const <Widget>[
SizedBox(height: 1000.0, width: 1000.0, child: Text('I Love Flutter!')),
],
);
},
),
),
);
}
await tester.pumpWidget(buildFrame(behavior1));
expect(controller.position.pixels, 0.0);
final Offset listLocation = tester.getCenter(find.byType(ListView));
final TestGesture gesture1 = await tester.createGesture(pointer: 1);
await gesture1.down(listLocation);
await tester.pump();
final TestGesture gesture2 = await tester.createGesture(pointer: 2);
await gesture2.down(listLocation);
await tester.pump();
await gesture1.moveBy(const Offset(0, -50));
await tester.pump();
await gesture2.moveBy(const Offset(0, -40));
await tester.pump();
// The default multitouchDragStrategy is latestPointer.
// Only the latest active pointer be tracked.
final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext);
expect(
scrollBehavior.getMultitouchDragStrategy(capturedContext),
MultitouchDragStrategy.latestPointer,
);
expect(controller.position.pixels, 40.0);
// Change to averageBoundaryPointers.
await tester.pumpWidget(buildFrame(behavior2));
await gesture1.moveBy(const Offset(0, -70));
await tester.pump();
await gesture2.moveBy(const Offset(0, -60));
await tester.pump();
expect(controller.position.pixels, 40.0 + 70.0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.android,
TargetPlatform.linux,
TargetPlatform.fuchsia,
TargetPlatform.windows,
}),
);
testWidgets(
'ScrollBehavior multitouchDragStrategy test (Apple platforms) - 3',
(WidgetTester tester) async {
const behavior1 = ScrollBehavior();
final ScrollBehavior behavior2 = const ScrollBehavior().copyWith(
multitouchDragStrategy: MultitouchDragStrategy.latestPointer,
);
final controller = ScrollController();
late BuildContext capturedContext;
addTearDown(() => controller.dispose());
Widget buildFrame(ScrollBehavior behavior) {
return Directionality(
textDirection: TextDirection.ltr,
child: ScrollConfiguration(
behavior: behavior,
child: Builder(
builder: (BuildContext context) {
capturedContext = context;
return ListView(
controller: controller,
children: const <Widget>[
SizedBox(height: 1000.0, width: 1000.0, child: Text('I Love Flutter!')),
],
);
},
),
),
);
}
await tester.pumpWidget(buildFrame(behavior1));
expect(controller.position.pixels, 0.0);
final Offset listLocation = tester.getCenter(find.byType(ListView));
final TestGesture gesture1 = await tester.createGesture(pointer: 1);
await gesture1.down(listLocation);
await tester.pump();
final TestGesture gesture2 = await tester.createGesture(pointer: 2);
await gesture2.down(listLocation);
await tester.pump();
await gesture1.moveBy(const Offset(0, -40));
await tester.pump();
await gesture2.moveBy(const Offset(0, -50));
await tester.pump();
// The default multitouchDragStrategy is averageBoundaryPointers.
final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext);
expect(
scrollBehavior.getMultitouchDragStrategy(capturedContext),
MultitouchDragStrategy.averageBoundaryPointers,
);
expect(controller.position.pixels, 50.0);
// Change to latestPointer.
await tester.pumpWidget(buildFrame(behavior2));
await gesture1.moveBy(const Offset(0, -50));
await tester.pump();
await gesture2.moveBy(const Offset(0, -40));
await tester.pump();
expect(controller.position.pixels, 50.0 + 40.0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.macOS,
}),
);
group('ScrollBehavior configuration is maintained over multiple copies', () {
testWidgets('dragDevices', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/91673
const 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('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 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 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));
});
}