mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
1818 lines
68 KiB
Dart
1818 lines
68 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 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'semantics_tester.dart';
|
|
|
|
Future<void> pumpTest(
|
|
WidgetTester tester,
|
|
TargetPlatform? platform, {
|
|
bool scrollable = true,
|
|
bool reverse = false,
|
|
Set<LogicalKeyboardKey>? axisModifier,
|
|
Axis scrollDirection = Axis.vertical,
|
|
ScrollController? controller,
|
|
bool enableMouseDrag = true,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
scrollBehavior: const NoScrollbarBehavior().copyWith(
|
|
dragDevices: enableMouseDrag
|
|
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
|
|
: null,
|
|
pointerAxisModifiers: axisModifier,
|
|
),
|
|
theme: ThemeData(platform: platform),
|
|
home: CustomScrollView(
|
|
controller: controller,
|
|
reverse: reverse,
|
|
scrollDirection: scrollDirection,
|
|
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: scrollDirection == Axis.vertical ? 2000.0 : null,
|
|
width: scrollDirection == Axis.horizontal ? 2000.0 : null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump(const Duration(seconds: 5)); // to let the theme animate
|
|
}
|
|
|
|
class NoScrollbarBehavior extends MaterialScrollBehavior {
|
|
const NoScrollbarBehavior();
|
|
|
|
@override
|
|
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
|
|
}
|
|
|
|
// Pump a nested scrollable. The outer scrollable contains a sliver of a
|
|
// 300-pixel-long scrollable followed by a 2000-pixel-long content.
|
|
Future<void> pumpDoubleScrollableTest(WidgetTester tester, TargetPlatform platform) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(platform: platform),
|
|
home: const CustomScrollView(
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: 300,
|
|
child: CustomScrollView(
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump(const Duration(seconds: 5)); // to let the theme animate
|
|
}
|
|
|
|
const double dragOffset = 200.0;
|
|
|
|
double getScrollOffset(WidgetTester tester, {bool last = true}) {
|
|
Finder viewportFinder = find.byType(Viewport);
|
|
if (last) {
|
|
viewportFinder = viewportFinder.last;
|
|
}
|
|
final RenderViewport viewport = tester.renderObject(viewportFinder);
|
|
return viewport.offset.pixels;
|
|
}
|
|
|
|
double getScrollVelocity(WidgetTester tester) {
|
|
final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
|
|
final position = viewport.offset as ScrollPosition;
|
|
return position.activity!.velocity;
|
|
}
|
|
|
|
void resetScrollOffset(WidgetTester tester) {
|
|
final RenderViewport viewport = tester.renderObject(find.byType(Viewport));
|
|
final position = viewport.offset as ScrollPosition;
|
|
position.jumpTo(0.0);
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('hitTestBehavior is respected', (WidgetTester tester) async {
|
|
HitTestBehavior? getBehavior(Type of) {
|
|
final RawGestureDetector widget = tester.widget(
|
|
find.descendant(of: find.byType(of), matching: find.byType(RawGestureDetector)),
|
|
);
|
|
return widget.behavior;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
const MaterialApp(home: SingleChildScrollView(hitTestBehavior: HitTestBehavior.translucent)),
|
|
);
|
|
expect(getBehavior(SingleChildScrollView), HitTestBehavior.translucent);
|
|
|
|
await tester.pumpWidget(
|
|
const MaterialApp(home: CustomScrollView(hitTestBehavior: HitTestBehavior.translucent)),
|
|
);
|
|
expect(getBehavior(CustomScrollView), HitTestBehavior.translucent);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(home: ListView(hitTestBehavior: HitTestBehavior.translucent)),
|
|
);
|
|
expect(getBehavior(ListView), HitTestBehavior.translucent);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: GridView.extent(maxCrossAxisExtent: 1, hitTestBehavior: HitTestBehavior.translucent),
|
|
),
|
|
);
|
|
expect(getBehavior(GridView), HitTestBehavior.translucent);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(home: PageView(hitTestBehavior: HitTestBehavior.translucent)),
|
|
);
|
|
expect(getBehavior(PageView), HitTestBehavior.translucent);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: ListWheelScrollView(
|
|
itemExtent: 10,
|
|
hitTestBehavior: HitTestBehavior.translucent,
|
|
children: const <Widget>[],
|
|
),
|
|
),
|
|
);
|
|
expect(getBehavior(ListWheelScrollView), HitTestBehavior.translucent);
|
|
});
|
|
|
|
testWidgets('hitTestBehavior.translucent lets widgets underneath catch the hit', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key key = UniqueKey();
|
|
var tapped = false;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Stack(
|
|
children: <Widget>[
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => tapped = true,
|
|
child: SizedBox(key: key, height: 300),
|
|
),
|
|
),
|
|
const SingleChildScrollView(hitTestBehavior: HitTestBehavior.translucent),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.tapAt(tester.getCenter(find.byKey(key)));
|
|
expect(tapped, isTrue);
|
|
});
|
|
|
|
testWidgets('Flings on different platforms', (WidgetTester tester) async {
|
|
await pumpTest(tester, TargetPlatform.android);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
expect(getScrollOffset(tester), dragOffset);
|
|
await tester.pump(); // trigger fling
|
|
expect(getScrollOffset(tester), dragOffset);
|
|
await tester.pump(const Duration(seconds: 5));
|
|
final double androidResult = getScrollOffset(tester);
|
|
|
|
resetScrollOffset(tester);
|
|
|
|
await pumpTest(tester, TargetPlatform.iOS);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
// Scroll starts ease into the scroll on iOS.
|
|
expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
|
|
await tester.pump(); // trigger fling
|
|
expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
|
|
await tester.pump(const Duration(seconds: 5));
|
|
final double iOSResult = getScrollOffset(tester);
|
|
|
|
resetScrollOffset(tester);
|
|
|
|
await pumpTest(tester, TargetPlatform.macOS);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
// Scroll starts ease into the scroll on iOS.
|
|
expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
|
|
await tester.pump(); // trigger fling
|
|
expect(getScrollOffset(tester), moreOrLessEquals(197.16666666666669));
|
|
await tester.pump(const Duration(seconds: 5));
|
|
final double macOSResult = getScrollOffset(tester);
|
|
|
|
expect(macOSResult, lessThan(androidResult)); // macOS is slipperier than Android
|
|
expect(androidResult, lessThan(iOSResult)); // iOS is slipperier than Android
|
|
expect(macOSResult, lessThan(iOSResult)); // iOS is slipperier than macOS
|
|
});
|
|
|
|
testWidgets(
|
|
'Holding scroll',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
await tester.drag(find.byType(Scrollable), const Offset(0.0, 200.0), touchSlopY: 0.0);
|
|
expect(getScrollOffset(tester), -200.0);
|
|
await tester.pump(); // trigger ballistic
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(getScrollOffset(tester), greaterThan(-200.0));
|
|
expect(getScrollOffset(tester), lessThan(0.0));
|
|
final double heldPosition = getScrollOffset(tester);
|
|
// Hold and let go while in overscroll.
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
expect(await tester.pumpAndSettle(), 1);
|
|
expect(getScrollOffset(tester), heldPosition);
|
|
await gesture.up();
|
|
// Once the hold is let go, it should still snap back to origin.
|
|
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Repeated flings builds momentum',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// Repeat the exact same motion.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump();
|
|
// On iOS, the velocity will be larger than the velocity of the last fling by a
|
|
// non-trivial amount.
|
|
expect(getScrollVelocity(tester), greaterThan(1100.0));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('Repeated flings do not build momentum on Android', (WidgetTester tester) async {
|
|
await pumpTest(tester, TargetPlatform.android);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// Repeat the exact same motion.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump();
|
|
// On Android, there is no momentum build. The final velocity is the same as the
|
|
// velocity of the last fling.
|
|
expect(getScrollVelocity(tester), moreOrLessEquals(1000.0));
|
|
});
|
|
|
|
testWidgets(
|
|
'A slower final fling does not apply carried momentum',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// Repeat the exact same motion to build momentum.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger the second fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// Make a final fling that is much slower.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 200.0);
|
|
await tester.pump(); // trigger the third fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// expect that there is no carried velocity
|
|
expect(getScrollVelocity(tester), lessThan(200.0));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'No iOS/macOS momentum build with flings in opposite directions',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
// Repeat the exact same motion in the opposite direction.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, dragOffset), 1000.0);
|
|
await tester.pump();
|
|
// The only applied velocity to the scrollable is the second fling that was in the
|
|
// opposite direction.
|
|
expect(getScrollVelocity(tester), -1000.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'No iOS/macOS momentum kept on hold gestures',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
await tester.pump(); // trigger fling
|
|
await tester.pump(const Duration(milliseconds: 10));
|
|
expect(getScrollVelocity(tester), greaterThan(0.0));
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await tester.pump(const Duration(milliseconds: 40));
|
|
await gesture.up();
|
|
// After a hold longer than 2 frames, previous velocity is lost.
|
|
expect(getScrollVelocity(tester), 0.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('Drags creeping unaffected on Android', (WidgetTester tester) async {
|
|
await pumpTest(tester, TargetPlatform.android);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await gesture.moveBy(const Offset(0.0, -0.5));
|
|
expect(getScrollOffset(tester), 0.5);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 10));
|
|
expect(getScrollOffset(tester), 1.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
|
|
expect(getScrollOffset(tester), 1.5);
|
|
});
|
|
|
|
testWidgets(
|
|
'Drags creeping must break threshold on iOS/macOS',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await gesture.moveBy(const Offset(0.0, -0.5));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 10));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 30));
|
|
// Now -2.5 in total.
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 40));
|
|
// Now -3.5, just reached threshold.
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 50));
|
|
// -0.5 over threshold transferred.
|
|
expect(getScrollOffset(tester), 0.5);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Big drag over threshold magnitude preserved on iOS/macOS',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await gesture.moveBy(const Offset(0.0, -30.0));
|
|
// No offset lost from threshold.
|
|
expect(getScrollOffset(tester), 30.0);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Slow threshold breaks are attenuated on iOS/macOS',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
// This is a typical 'hesitant' iOS scroll start.
|
|
await gesture.moveBy(const Offset(0.0, -10.0));
|
|
expect(getScrollOffset(tester), moreOrLessEquals(1.1666666666666667));
|
|
await gesture.moveBy(const Offset(0.0, -10.0), timeStamp: const Duration(milliseconds: 20));
|
|
// Subsequent motions unaffected.
|
|
expect(getScrollOffset(tester), moreOrLessEquals(11.16666666666666673));
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Small continuing motion preserved on iOS/macOS',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
|
|
expect(getScrollOffset(tester), 30.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
|
|
expect(getScrollOffset(tester), 30.5);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 40));
|
|
expect(getScrollOffset(tester), 31.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 60));
|
|
expect(getScrollOffset(tester), 31.5);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Motion stop resets threshold on iOS/macOS',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
);
|
|
await gesture.moveBy(const Offset(0.0, -30.0)); // Break threshold.
|
|
expect(getScrollOffset(tester), 30.0);
|
|
await gesture.moveBy(const Offset(0.0, -0.5), timeStamp: const Duration(milliseconds: 20));
|
|
expect(getScrollOffset(tester), 30.5);
|
|
await gesture.moveBy(Offset.zero, timeStamp: const Duration(milliseconds: 21));
|
|
// Stationary too long, threshold reset.
|
|
await gesture.moveBy(Offset.zero, timeStamp: const Duration(milliseconds: 120));
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 140));
|
|
expect(getScrollOffset(tester), 30.5);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 150));
|
|
expect(getScrollOffset(tester), 30.5);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 160));
|
|
expect(getScrollOffset(tester), 30.5);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 170));
|
|
// New threshold broken.
|
|
expect(getScrollOffset(tester), 31.5);
|
|
await gesture.moveBy(const Offset(0.0, -1.0), timeStamp: const Duration(milliseconds: 180));
|
|
expect(getScrollOffset(tester), 32.5);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets('Scroll pointer signals are handled on Fuchsia', (WidgetTester tester) async {
|
|
await pumpTest(tester, TargetPlatform.fuchsia);
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
expect(getScrollOffset(tester), 20.0);
|
|
// Pointer signals should not cause overscroll.
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
});
|
|
|
|
testWidgets('Scroll pointer signals are handled when there is competition', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// This is a regression test. When there are multiple scrollables listening
|
|
// to the same event, for example when scrollables are nested, there used
|
|
// to be exceptions at scrolling events.
|
|
|
|
await pumpDoubleScrollableTest(tester, TargetPlatform.fuchsia);
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport).last);
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
expect(getScrollOffset(tester), 20.0);
|
|
// Pointer signals should not cause overscroll.
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
});
|
|
|
|
testWidgets('Scroll pointer signals are ignored when scrolling is disabled', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await pumpTest(tester, TargetPlatform.fuchsia, scrollable: false);
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
expect(getScrollOffset(tester), 0.0);
|
|
});
|
|
|
|
testWidgets(
|
|
'Engine is notified of ignored pointer signals (no scroll physics)',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride, scrollable: false);
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
|
|
var allowedPlatformDefault = false;
|
|
await tester.sendEventToBinding(
|
|
testPointer.scroll(
|
|
const Offset(0.0, 20.0),
|
|
onRespond: ({required bool allowPlatformDefault}) {
|
|
allowedPlatformDefault = allowPlatformDefault;
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(
|
|
allowedPlatformDefault,
|
|
isTrue,
|
|
reason: 'Engine should be notified of ignored scroll pointer signals.',
|
|
);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets(
|
|
'Engine is notified of rejected scroll events (wrong direction)',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride, scrollDirection: Axis.horizontal);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
|
|
// Horizontal input is accepted
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
await tester.sendEventToBinding(
|
|
testPointer.scroll(
|
|
const Offset(0.0, 10.0),
|
|
onRespond: ({required bool allowPlatformDefault}) {
|
|
fail('The engine should not be notified when the scroll is accepted.');
|
|
},
|
|
),
|
|
);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
await tester.pump();
|
|
|
|
// Vertical input not accepted
|
|
var allowedPlatformDefault = false;
|
|
await tester.sendEventToBinding(
|
|
testPointer.scroll(
|
|
const Offset(0.0, 20.0),
|
|
onRespond: ({required bool allowPlatformDefault}) {
|
|
allowedPlatformDefault = allowPlatformDefault;
|
|
},
|
|
),
|
|
);
|
|
expect(
|
|
allowedPlatformDefault,
|
|
isTrue,
|
|
reason: 'Engine should be notified when scroll is rejected by the scrollable.',
|
|
);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
testWidgets(
|
|
'Holding scroll and Scroll pointer signal will update ScrollDirection.forward / ScrollDirection.reverse',
|
|
(WidgetTester tester) async {
|
|
ScrollDirection? lastUserScrollingDirection;
|
|
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await pumpTest(tester, TargetPlatform.fuchsia, controller: controller);
|
|
|
|
controller.addListener(() {
|
|
if (controller.position.userScrollDirection != ScrollDirection.idle) {
|
|
lastUserScrollingDirection = controller.position.userScrollDirection;
|
|
}
|
|
});
|
|
|
|
await tester.drag(find.byType(Scrollable), const Offset(0.0, -20.0), touchSlopY: 0.0);
|
|
|
|
expect(lastUserScrollingDirection, ScrollDirection.reverse);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
|
|
expect(lastUserScrollingDirection, ScrollDirection.reverse);
|
|
|
|
await tester.drag(find.byType(Scrollable), const Offset(0.0, 20.0), touchSlopY: 0.0);
|
|
|
|
expect(lastUserScrollingDirection, ScrollDirection.forward);
|
|
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -20.0)));
|
|
|
|
expect(lastUserScrollingDirection, ScrollDirection.forward);
|
|
},
|
|
);
|
|
|
|
testWidgets('Scrolls in correct direction when scroll axis is reversed', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await pumpTest(tester, TargetPlatform.fuchsia, reverse: true);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -20.0)));
|
|
|
|
expect(getScrollOffset(tester), 20.0);
|
|
});
|
|
|
|
testWidgets('Scrolls horizontally when shift is pressed by default', (WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride, scrollDirection: Axis.horizontal);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input flipped to horizontal and accepted.
|
|
expect(getScrollOffset(tester), 20.0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
await tester.pump();
|
|
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 20.0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Scroll axis is not flipped for trackpad', (WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride, scrollDirection: Axis.horizontal);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.trackpad);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not flipped.
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
|
|
await tester.pump();
|
|
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 0.0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets('Scrolls horizontally when custom key is pressed', (WidgetTester tester) async {
|
|
await pumpTest(
|
|
tester,
|
|
debugDefaultTargetPlatformOverride,
|
|
scrollDirection: Axis.horizontal,
|
|
axisModifier: <LogicalKeyboardKey>{LogicalKeyboardKey.altLeft},
|
|
);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input flipped to horizontal and accepted.
|
|
expect(getScrollOffset(tester), 20.0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.pump();
|
|
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 20.0);
|
|
}, variant: TargetPlatformVariant.all());
|
|
|
|
testWidgets(
|
|
'Still scrolls horizontally when other keys are pressed at the same time',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(
|
|
tester,
|
|
debugDefaultTargetPlatformOverride,
|
|
scrollDirection: Axis.horizontal,
|
|
axisModifier: <LogicalKeyboardKey>{LogicalKeyboardKey.altLeft},
|
|
);
|
|
|
|
final Offset scrollEventLocation = tester.getCenter(find.byType(Viewport));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
|
testPointer.hover(scrollEventLocation);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.space);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical flipped & accepted.
|
|
expect(getScrollOffset(tester), 20.0);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
|
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.space);
|
|
await tester.pump();
|
|
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
// Vertical input not accepted
|
|
expect(getScrollOffset(tester), 20.0);
|
|
},
|
|
variant: TargetPlatformVariant.all(),
|
|
);
|
|
|
|
group('setCanDrag to false with active drag gesture: ', () {
|
|
Future<void> pumpTestWidget(WidgetTester tester, {required bool canDrag}) {
|
|
return tester.pumpWidget(
|
|
MaterialApp(
|
|
home: CustomScrollView(
|
|
physics: canDrag
|
|
? const AlwaysScrollableScrollPhysics()
|
|
: const NeverScrollableScrollPhysics(),
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(height: 2000, child: GestureDetector(onTap: () {})),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
testWidgets('Hold does not disable user interaction', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/66816.
|
|
await pumpTestWidget(tester, canDrag: true);
|
|
final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>(
|
|
find.descendant(of: find.byType(CustomScrollView), matching: find.byType(IgnorePointer)),
|
|
);
|
|
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Viewport)),
|
|
);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
await pumpTestWidget(tester, canDrag: false);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
await gesture.up();
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
});
|
|
|
|
testWidgets('Drag disables user interaction when recognized', (WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/66816.
|
|
await pumpTestWidget(tester, canDrag: true);
|
|
final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>(
|
|
find.descendant(of: find.byType(CustomScrollView), matching: find.byType(IgnorePointer)),
|
|
);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Viewport)),
|
|
);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
await gesture.moveBy(const Offset(0, -100));
|
|
// Starts ignoring when the drag is recognized.
|
|
expect(renderIgnorePointer.ignoring, true);
|
|
|
|
await pumpTestWidget(tester, canDrag: false);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
await gesture.up();
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
});
|
|
|
|
testWidgets('Ballistic disables user interaction until it stops', (WidgetTester tester) async {
|
|
await pumpTestWidget(tester, canDrag: true);
|
|
final RenderIgnorePointer renderIgnorePointer = tester.renderObject<RenderIgnorePointer>(
|
|
find.descendant(of: find.byType(CustomScrollView), matching: find.byType(IgnorePointer)),
|
|
);
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
|
|
// Starts ignoring when the drag is recognized.
|
|
await tester.fling(find.byType(Scrollable), const Offset(0, -100), 1000);
|
|
expect(renderIgnorePointer.ignoring, true);
|
|
await tester.pump();
|
|
|
|
// When the activity ends we should stop ignoring pointers.
|
|
await tester.pumpAndSettle();
|
|
expect(renderIgnorePointer.ignoring, false);
|
|
});
|
|
});
|
|
|
|
testWidgets('Can recommendDeferredLoadingForContext - animation', (WidgetTester tester) async {
|
|
final widgetTracker = <String>[];
|
|
var cheapWidgets = 0;
|
|
var expensiveWidgets = 0;
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView.builder(
|
|
controller: controller,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
if (Scrollable.recommendDeferredLoadingForContext(context)) {
|
|
cheapWidgets += 1;
|
|
widgetTracker.add('cheap');
|
|
return const SizedBox(height: 50.0);
|
|
}
|
|
widgetTracker.add('expensive');
|
|
expensiveWidgets += 1;
|
|
return const SizedBox(height: 50.0);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(expensiveWidgets, 17);
|
|
expect(cheapWidgets, 0);
|
|
|
|
// The position value here is different from the maximum velocity we will
|
|
// reach, which is controlled by a combination of curve, duration, and
|
|
// position.
|
|
// This is just meant to be a pretty good simulation. A linear curve
|
|
// with these same parameters will never back off on the velocity enough
|
|
// to reset here.
|
|
controller.animateTo(5000, duration: const Duration(seconds: 2), curve: Curves.linear);
|
|
|
|
expect(expensiveWidgets, 17);
|
|
expect(widgetTracker.every((String type) => type == 'expensive'), true);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(expensiveWidgets, 17);
|
|
expect(cheapWidgets, 25);
|
|
expect(widgetTracker.skip(17).every((String type) => type == 'cheap'), true);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(expensiveWidgets, 22);
|
|
expect(cheapWidgets, 95);
|
|
expect(widgetTracker.skip(17).skip(25).take(70).every((String type) => type == 'cheap'), true);
|
|
expect(
|
|
widgetTracker.skip(17).skip(25).skip(70).every((String type) => type == 'expensive'),
|
|
true,
|
|
);
|
|
});
|
|
|
|
testWidgets('Can recommendDeferredLoadingForContext - ballistics', (WidgetTester tester) async {
|
|
var cheapWidgets = 0;
|
|
var expensiveWidgets = 0;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView.builder(
|
|
itemBuilder: (BuildContext context, int index) {
|
|
if (Scrollable.recommendDeferredLoadingForContext(context)) {
|
|
cheapWidgets += 1;
|
|
return const SizedBox(height: 50.0);
|
|
}
|
|
expensiveWidgets += 1;
|
|
return SizedBox(key: ValueKey<String>('Box $index'), height: 50.0);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey<String>('Box 52')), findsNothing);
|
|
|
|
expect(expensiveWidgets, 17);
|
|
expect(cheapWidgets, 0);
|
|
|
|
// Getting the tester to simulate a life-like fling is difficult.
|
|
// Instead, just manually drive the activity with a ballistic simulation as
|
|
// if the user has flung the list.
|
|
Scrollable.of(
|
|
find.byType(SizedBox).evaluate().first,
|
|
).position.activity!.delegate.goBallistic(4000);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
|
expect(find.byKey(const ValueKey<String>('Box 52')), findsOneWidget);
|
|
|
|
expect(expensiveWidgets, 40);
|
|
expect(cheapWidgets, 21);
|
|
});
|
|
|
|
testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (
|
|
WidgetTester tester,
|
|
) async {
|
|
var cheapWidgets = 0;
|
|
var expensiveWidgets = 0;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView.builder(
|
|
physics: SuperPessimisticScrollPhysics(),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
if (Scrollable.recommendDeferredLoadingForContext(context)) {
|
|
cheapWidgets += 1;
|
|
return SizedBox(key: ValueKey<String>('Cheap box $index'), height: 50.0);
|
|
}
|
|
expensiveWidgets += 1;
|
|
return SizedBox(key: ValueKey<String>('Box $index'), height: 50.0);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final ScrollPosition position = Scrollable.of(find.byType(SizedBox).evaluate().first).position;
|
|
final physics = position.physics as SuperPessimisticScrollPhysics;
|
|
|
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsNothing);
|
|
|
|
expect(physics.count, 17);
|
|
expect(expensiveWidgets, 17);
|
|
expect(cheapWidgets, 0);
|
|
|
|
// Getting the tester to simulate a life-like fling is difficult.
|
|
// Instead, just manually drive the activity with a ballistic simulation as
|
|
// if the user has flung the list.
|
|
position.activity!.delegate.goBallistic(4000);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
|
|
|
expect(expensiveWidgets, 17);
|
|
expect(cheapWidgets, 44);
|
|
expect(physics.count, 44 + 17);
|
|
});
|
|
|
|
testWidgets(
|
|
'Can recommendDeferredLoadingForContext - override heuristic and always return true',
|
|
(WidgetTester tester) async {
|
|
var cheapWidgets = 0;
|
|
var expensiveWidgets = 0;
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: ListView.builder(
|
|
physics: const ExtraSuperPessimisticScrollPhysics(),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
if (Scrollable.recommendDeferredLoadingForContext(context)) {
|
|
cheapWidgets += 1;
|
|
return SizedBox(key: ValueKey<String>('Cheap box $index'), height: 50.0);
|
|
}
|
|
expensiveWidgets += 1;
|
|
return SizedBox(key: ValueKey<String>('Box $index'), height: 50.0);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
final ScrollPosition position = Scrollable.of(
|
|
find.byType(SizedBox).evaluate().first,
|
|
).position;
|
|
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 0')), findsOneWidget);
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsNothing);
|
|
|
|
expect(expensiveWidgets, 0);
|
|
expect(cheapWidgets, 17);
|
|
|
|
// Getting the tester to simulate a life-like fling is difficult.
|
|
// Instead, just manually drive the activity with a ballistic simulation as
|
|
// if the user has flung the list.
|
|
position.activity!.delegate.goBallistic(4000);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 0')), findsNothing);
|
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
|
|
|
expect(expensiveWidgets, 0);
|
|
expect(cheapWidgets, 61);
|
|
},
|
|
);
|
|
|
|
testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async {
|
|
final controller = PageController();
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: PageView(
|
|
controller: controller,
|
|
children: List<ListView>.generate(3, (int pageIndex) {
|
|
return ListView(
|
|
key: Key('list_$pageIndex'),
|
|
children: List<Widget>.generate(100, (int listIndex) {
|
|
return Row(
|
|
children: <Widget>[
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_0'),
|
|
color: Colors.red,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_1'),
|
|
color: Colors.blue,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_2'),
|
|
color: Colors.green,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder targetMidRightPage0 = find.byKey(const Key('0_25_2'));
|
|
final Finder targetMidRightPage1 = find.byKey(const Key('1_25_2'));
|
|
final Finder targetMidLeftPage1 = find.byKey(const Key('1_25_0'));
|
|
|
|
expect(find.byKey(const Key('list_0')), findsOneWidget);
|
|
expect(find.byKey(const Key('list_1')), findsNothing);
|
|
expect(targetMidRightPage0, findsOneWidget);
|
|
expect(targetMidRightPage1, findsNothing);
|
|
expect(targetMidLeftPage1, findsNothing);
|
|
|
|
await tester.ensureVisible(targetMidRightPage0);
|
|
await tester.pumpAndSettle();
|
|
expect(targetMidRightPage0, findsOneWidget);
|
|
expect(targetMidRightPage1, findsNothing);
|
|
expect(targetMidLeftPage1, findsNothing);
|
|
|
|
controller.jumpToPage(1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('list_0')), findsNothing);
|
|
expect(find.byKey(const Key('list_1')), findsOneWidget);
|
|
await tester.ensureVisible(targetMidRightPage1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(targetMidRightPage0, findsNothing);
|
|
expect(targetMidRightPage1, findsOneWidget);
|
|
expect(targetMidLeftPage1, findsOneWidget);
|
|
|
|
await tester.ensureVisible(targetMidLeftPage1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(targetMidRightPage0, findsNothing);
|
|
expect(targetMidRightPage1, findsOneWidget);
|
|
expect(targetMidLeftPage1, findsOneWidget);
|
|
});
|
|
|
|
testWidgets('ensureVisible does not move TabViews', (WidgetTester tester) async {
|
|
final TickerProvider vsync = TestTickerProvider();
|
|
final controller = TabController(length: 3, vsync: vsync);
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: TabBarView(
|
|
controller: controller,
|
|
children: List<ListView>.generate(3, (int pageIndex) {
|
|
return ListView(
|
|
key: Key('list_$pageIndex'),
|
|
children: List<Widget>.generate(100, (int listIndex) {
|
|
return Row(
|
|
children: <Widget>[
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_0'),
|
|
color: Colors.red,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_1'),
|
|
color: Colors.blue,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
Container(
|
|
key: Key('${pageIndex}_${listIndex}_2'),
|
|
color: Colors.green,
|
|
width: 200,
|
|
height: 10,
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder targetMidRightPage0 = find.byKey(const Key('0_25_2'));
|
|
final Finder targetMidRightPage1 = find.byKey(const Key('1_25_2'));
|
|
final Finder targetMidLeftPage1 = find.byKey(const Key('1_25_0'));
|
|
|
|
expect(find.byKey(const Key('list_0')), findsOneWidget);
|
|
expect(find.byKey(const Key('list_1')), findsNothing);
|
|
expect(targetMidRightPage0, findsOneWidget);
|
|
expect(targetMidRightPage1, findsNothing);
|
|
expect(targetMidLeftPage1, findsNothing);
|
|
|
|
await tester.ensureVisible(targetMidRightPage0);
|
|
await tester.pumpAndSettle();
|
|
expect(targetMidRightPage0, findsOneWidget);
|
|
expect(targetMidRightPage1, findsNothing);
|
|
expect(targetMidLeftPage1, findsNothing);
|
|
|
|
controller.index = 1;
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('list_0')), findsNothing);
|
|
expect(find.byKey(const Key('list_1')), findsOneWidget);
|
|
await tester.ensureVisible(targetMidRightPage1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(targetMidRightPage0, findsNothing);
|
|
expect(targetMidRightPage1, findsOneWidget);
|
|
expect(targetMidLeftPage1, findsOneWidget);
|
|
|
|
await tester.ensureVisible(targetMidLeftPage1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(targetMidRightPage0, findsNothing);
|
|
expect(targetMidRightPage1, findsOneWidget);
|
|
expect(targetMidLeftPage1, findsOneWidget);
|
|
});
|
|
|
|
testWidgets('PointerScroll on nested NeverScrollable ListView goes to outer Scrollable.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/70948
|
|
final outerController = ScrollController();
|
|
addTearDown(outerController.dispose);
|
|
final innerController = ScrollController();
|
|
addTearDown(innerController.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(useMaterial3: false),
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
controller: outerController,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: <Widget>[
|
|
Column(
|
|
children: <Widget>[
|
|
for (int i = 0; i < 100; i++) Text('SingleChildScrollView $i'),
|
|
],
|
|
),
|
|
SizedBox(
|
|
height: 3000,
|
|
width: 400,
|
|
child: ListView.builder(
|
|
controller: innerController,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: 100,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return Text('Nested NeverScrollable ListView $index');
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(outerController.position.pixels, 0.0);
|
|
expect(innerController.position.pixels, 0.0);
|
|
final Offset outerScrollable = tester.getCenter(find.text('SingleChildScrollView 3'));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
// Hover over the outer scroll view and create a pointer scroll.
|
|
testPointer.hover(outerScrollable);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)));
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
expect(outerController.position.pixels, 20.0);
|
|
expect(innerController.position.pixels, 0.0);
|
|
|
|
final Offset innerScrollable = tester.getCenter(
|
|
find.text('Nested NeverScrollable ListView 20'),
|
|
);
|
|
// Hover over the inner scroll view and create a pointer scroll.
|
|
// This inner scroll view is not scrollable, and so the outer should scroll.
|
|
testPointer.hover(innerScrollable);
|
|
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -20.0)));
|
|
await tester.pump(const Duration(milliseconds: 250));
|
|
expect(outerController.position.pixels, 0.0);
|
|
expect(innerController.position.pixels, 0.0);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/71949
|
|
testWidgets('Zero offset pointer scroll should not trigger an assertion.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final controller = ScrollController();
|
|
addTearDown(controller.dispose);
|
|
|
|
Widget build(double height) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: double.infinity,
|
|
height: height,
|
|
child: SingleChildScrollView(
|
|
controller: controller,
|
|
child: const SizedBox(width: double.infinity, height: 300.0),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(build(200.0));
|
|
expect(controller.position.pixels, 0.0);
|
|
|
|
controller.jumpTo(100.0);
|
|
expect(controller.position.pixels, 100.0);
|
|
|
|
// Make the outer constraints larger that the scrollable widget is no longer able to scroll.
|
|
await tester.pumpWidget(build(300.0));
|
|
expect(controller.position.pixels, 0.0);
|
|
expect(controller.position.maxScrollExtent, 0.0);
|
|
|
|
// Hover over the scroll view and create a zero offset pointer scroll.
|
|
final Offset scrollable = tester.getCenter(find.byType(SingleChildScrollView));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
testPointer.hover(scrollable);
|
|
await tester.sendEventToBinding(testPointer.scroll(Offset.zero));
|
|
|
|
expect(tester.takeException(), null);
|
|
});
|
|
|
|
testWidgets(
|
|
'Accepts drag with unknown device kind by default',
|
|
(WidgetTester tester) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/90912.
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: CustomScrollView(
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.unknown,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 200);
|
|
|
|
await gesture.moveBy(const Offset(0.0, 200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await gesture.removePointer();
|
|
await tester.pump();
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
TargetPlatform.android,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'Does not scroll with mouse pointer drag when behavior is configured to ignore them',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: false);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await gesture.moveBy(const Offset(0.0, 200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await gesture.removePointer();
|
|
await tester.pump();
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
TargetPlatform.android,
|
|
}),
|
|
);
|
|
|
|
testWidgets("Support updating 'ScrollBehavior.dragDevices' at runtime", (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/111716
|
|
Widget buildFrame(Set<ui.PointerDeviceKind>? dragDevices) {
|
|
return MaterialApp(
|
|
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: dragDevices),
|
|
home: ListView.builder(
|
|
itemCount: 1000,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return Text('Item $index');
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(<ui.PointerDeviceKind>{ui.PointerDeviceKind.mouse}));
|
|
await tester.drag(
|
|
find.byType(Scrollable),
|
|
const Offset(0.0, -100.0),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
|
|
// Matching device should allow user scrolling.
|
|
expect(getScrollOffset(tester), 100.0);
|
|
|
|
await tester.pumpWidget(buildFrame(<ui.PointerDeviceKind>{ui.PointerDeviceKind.stylus}));
|
|
await tester.drag(
|
|
find.byType(Scrollable),
|
|
const Offset(0.0, -100.0),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
|
|
// Non-matching device should not allow user scrolling.
|
|
expect(getScrollOffset(tester), 100.0);
|
|
|
|
await tester.drag(
|
|
find.byType(Scrollable),
|
|
const Offset(0.0, -100.0),
|
|
kind: ui.PointerDeviceKind.stylus,
|
|
);
|
|
|
|
// Matching device should allow user scrolling.
|
|
expect(getScrollOffset(tester), 200.0);
|
|
});
|
|
|
|
testWidgets(
|
|
'Does scroll with mouse pointer drag when behavior is not configured to ignore them',
|
|
(WidgetTester tester) async {
|
|
await pumpTest(tester, debugDefaultTargetPlatformOverride);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 200.0);
|
|
|
|
await gesture.moveBy(const Offset(0.0, 200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
await gesture.removePointer();
|
|
await tester.pump();
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
TargetPlatform.android,
|
|
}),
|
|
);
|
|
|
|
testWidgets('Updated content dimensions correctly reflect in semantics', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/40419.
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
final listView = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: TickerMode(
|
|
enabled: true,
|
|
child: ListView.builder(
|
|
key: listView,
|
|
itemCount: 100,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return Text('Item $index');
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
SemanticsNode scrollableNode = tester.getSemantics(
|
|
find.descendant(of: find.byKey(listView), matching: find.byType(RawGestureDetector)),
|
|
);
|
|
SemanticsNode? syntheticScrollableNode;
|
|
scrollableNode.visitChildren((SemanticsNode node) {
|
|
syntheticScrollableNode = node;
|
|
return true;
|
|
});
|
|
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
|
|
// Disabled the ticker mode to trigger didChangeDependencies on Scrollable.
|
|
// This can happen when a route is push or pop from top.
|
|
// It will reconstruct the scroll position and apply content dimensions.
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: TickerMode(
|
|
enabled: false,
|
|
child: ListView.builder(
|
|
key: listView,
|
|
itemCount: 100,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return Text('Item $index');
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
// The correct workflow will be the following:
|
|
// 1. _RenderScrollSemantics receives a new scroll position without content
|
|
// dimensions and creates a SemanticsNode without implicit scroll.
|
|
// 2. The content dimensions are applied to the scroll position during the
|
|
// layout phase, and the scroll position marks the semantics node of
|
|
// _RenderScrollSemantics dirty.
|
|
// 3. The _RenderScrollSemantics rebuilds its semantics node with implicit
|
|
// scroll.
|
|
scrollableNode = tester.getSemantics(
|
|
find.descendant(of: find.byKey(listView), matching: find.byType(RawGestureDetector)),
|
|
);
|
|
syntheticScrollableNode = null;
|
|
scrollableNode.visitChildren((SemanticsNode node) {
|
|
syntheticScrollableNode = node;
|
|
return true;
|
|
});
|
|
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('Two panel semantics is added to the sibling nodes of direct children', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final SemanticsHandle handle = tester.ensureSemantics();
|
|
final key = UniqueKey();
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListView(
|
|
key: key,
|
|
children: const <Widget>[
|
|
TextField(autofocus: true, decoration: InputDecoration(prefixText: 'prefix')),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// Wait for focus.
|
|
await tester.pumpAndSettle();
|
|
|
|
final SemanticsNode scrollableNode = tester.getSemantics(find.byKey(key));
|
|
SemanticsNode? intermediateNode;
|
|
scrollableNode.visitChildren((SemanticsNode node) {
|
|
intermediateNode = node;
|
|
return true;
|
|
});
|
|
SemanticsNode? syntheticScrollableNode;
|
|
intermediateNode!.visitChildren((SemanticsNode node) {
|
|
syntheticScrollableNode = node;
|
|
return true;
|
|
});
|
|
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
|
|
|
|
var numberOfChild = 0;
|
|
syntheticScrollableNode!.visitChildren((SemanticsNode node) {
|
|
expect(node.isTagged(RenderViewport.useTwoPaneSemantics), isTrue);
|
|
numberOfChild += 1;
|
|
return true;
|
|
});
|
|
expect(numberOfChild, 2);
|
|
|
|
handle.dispose();
|
|
});
|
|
|
|
testWidgets('Scroll inertia cancel event', (WidgetTester tester) async {
|
|
await pumpTest(tester, null);
|
|
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
|
|
expect(getScrollOffset(tester), dragOffset);
|
|
await tester.pump(); // trigger fling
|
|
expect(getScrollOffset(tester), dragOffset);
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
|
|
await tester.sendEventToBinding(testPointer.hover(tester.getCenter(find.byType(Scrollable))));
|
|
await tester.sendEventToBinding(testPointer.scrollInertiaCancel()); // Cancel partway through.
|
|
await tester.pump();
|
|
expect(getScrollOffset(tester), closeTo(344.0642, 0.0001));
|
|
await tester.pump(const Duration(milliseconds: 4800));
|
|
expect(getScrollOffset(tester), closeTo(344.0642, 0.0001));
|
|
});
|
|
|
|
testWidgets('Swapping viewports in a scrollable does not crash', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
final GlobalKey key = GlobalKey();
|
|
final GlobalKey key1 = GlobalKey();
|
|
Widget buildScrollable(bool withViewPort) {
|
|
return Scrollable(
|
|
key: key,
|
|
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
|
if (withViewPort) {
|
|
final offset = ViewportOffset.zero();
|
|
addTearDown(() => offset.dispose());
|
|
return Viewport(
|
|
slivers: <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: Semantics(key: key1, container: true, child: const Text('text1')),
|
|
),
|
|
],
|
|
offset: offset,
|
|
);
|
|
}
|
|
return Semantics(key: key1, container: true, child: const Text('text1'));
|
|
},
|
|
);
|
|
}
|
|
|
|
// This should cache the inner node in Scrollable with the children text1.
|
|
await tester.pumpWidget(MaterialApp(home: buildScrollable(true)));
|
|
expect(semantics, includesNodeWith(tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics}));
|
|
// This does not use two panel, this should clear cached inner node.
|
|
await tester.pumpWidget(MaterialApp(home: buildScrollable(false)));
|
|
expect(
|
|
semantics,
|
|
isNot(includesNodeWith(tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics})),
|
|
);
|
|
// If the inner node was cleared in the previous step, this should not crash.
|
|
await tester.pumpWidget(MaterialApp(home: buildScrollable(true)));
|
|
expect(semantics, includesNodeWith(tags: <SemanticsTag>{RenderViewport.useTwoPaneSemantics}));
|
|
expect(tester.takeException(), isNull);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('deltaToScrollOrigin getter', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: CustomScrollView(
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.unknown,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 200);
|
|
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
|
|
expect(scrollable.deltaToScrollOrigin, const Offset(0.0, 200));
|
|
});
|
|
|
|
testWidgets('resolvedPhysics getter', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: ThemeData(platform: TargetPlatform.android),
|
|
home: const CustomScrollView(
|
|
physics: AlwaysScrollableScrollPhysics(),
|
|
slivers: <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
);
|
|
final TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.unknown,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pump();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(getScrollOffset(tester), 200);
|
|
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
|
|
String types(ScrollPhysics? value) => value!.parent == null
|
|
? '${value.runtimeType}'
|
|
: '${value.runtimeType} ${types(value.parent)}';
|
|
|
|
expect(
|
|
types(scrollable.resolvedPhysics),
|
|
'AlwaysScrollableScrollPhysics ClampingScrollPhysics RangeMaintainingScrollPhysics',
|
|
);
|
|
});
|
|
|
|
testWidgets('dragDevices change updates widget', (WidgetTester tester) async {
|
|
var enable = false;
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Scrollable(
|
|
scrollBehavior: const MaterialScrollBehavior().copyWith(
|
|
dragDevices: <ui.PointerDeviceKind>{if (enable) ui.PointerDeviceKind.mouse},
|
|
),
|
|
viewportBuilder: (BuildContext context, ViewportOffset position) => Viewport(
|
|
offset: position,
|
|
slivers: const <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
enable = !enable;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// Gesture should not work.
|
|
TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pumpAndSettle();
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
// Change state to include mouse pointer device.
|
|
await tester.tap(find.byType(FloatingActionButton));
|
|
await tester.pump();
|
|
|
|
// Gesture should work after state change.
|
|
gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pumpAndSettle();
|
|
expect(getScrollOffset(tester), 200);
|
|
});
|
|
|
|
testWidgets('dragDevices change updates widget when oldWidget scrollBehavior is null', (
|
|
WidgetTester tester,
|
|
) async {
|
|
ScrollBehavior? scrollBehavior;
|
|
|
|
await tester.pumpWidget(
|
|
Builder(
|
|
builder: (BuildContext context) {
|
|
return StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return MaterialApp(
|
|
home: Scaffold(
|
|
body: Scrollable(
|
|
physics: const ScrollPhysics(),
|
|
scrollBehavior: scrollBehavior,
|
|
viewportBuilder: (BuildContext context, ViewportOffset position) => Viewport(
|
|
offset: position,
|
|
slivers: const <Widget>[SliverToBoxAdapter(child: SizedBox(height: 2000.0))],
|
|
),
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
scrollBehavior = const MaterialScrollBehavior().copyWith(
|
|
dragDevices: <ui.PointerDeviceKind>{ui.PointerDeviceKind.mouse},
|
|
);
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
|
|
// Gesture should not work.
|
|
TestGesture gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pumpAndSettle();
|
|
expect(getScrollOffset(tester), 0.0);
|
|
|
|
// Change state to include mouse pointer device.
|
|
await tester.tap(find.byType(FloatingActionButton));
|
|
await tester.pump();
|
|
|
|
// Gesture should work after state change.
|
|
gesture = await tester.startGesture(
|
|
tester.getCenter(find.byType(Scrollable), warnIfMissed: true),
|
|
kind: ui.PointerDeviceKind.mouse,
|
|
);
|
|
expect(getScrollOffset(tester), 0.0);
|
|
await gesture.moveBy(const Offset(0.0, -200));
|
|
await tester.pumpAndSettle();
|
|
expect(getScrollOffset(tester), 200);
|
|
});
|
|
}
|
|
|
|
// ignore: must_be_immutable
|
|
class SuperPessimisticScrollPhysics extends ScrollPhysics {
|
|
SuperPessimisticScrollPhysics({super.parent});
|
|
|
|
int count = 0;
|
|
|
|
@override
|
|
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
|
|
count++;
|
|
return velocity > 1;
|
|
}
|
|
|
|
@override
|
|
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
|
return SuperPessimisticScrollPhysics(parent: buildParent(ancestor));
|
|
}
|
|
}
|
|
|
|
class ExtraSuperPessimisticScrollPhysics extends ScrollPhysics {
|
|
const ExtraSuperPessimisticScrollPhysics({super.parent});
|
|
|
|
@override
|
|
bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
|
return ExtraSuperPessimisticScrollPhysics(parent: buildParent(ancestor));
|
|
}
|
|
}
|
|
|
|
class TestTickerProvider extends TickerProvider {
|
|
@override
|
|
Ticker createTicker(TickerCallback onTick) {
|
|
return Ticker(onTick);
|
|
}
|
|
}
|