blob: 84ff11748ce3a0f38b5268612bebf3fd2997a113 [file] [log] [blame]
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
const CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness(
color: Color(0x59000000),
darkColor: Color(0x80FFFFFF),
);
void main() {
const Duration kScrollbarTimeToFade = Duration(milliseconds: 1200);
const Duration kScrollbarFadeDuration = Duration(milliseconds: 250);
const Duration kScrollbarResizeDuration = Duration(milliseconds: 100);
const Duration kLongPressDuration = Duration(milliseconds: 100);
testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(),
child: CupertinoScrollbar(
child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
),
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, -10.0));
await tester.pump();
// Scrollbar fully showing
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
await tester.pump(const Duration(seconds: 3));
await tester.pump(const Duration(seconds: 3));
// Still there.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
await gesture.up();
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration * 0.5);
// Opacity going down now.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color.withAlpha(69),
));
});
testWidgets('Scrollbar dark mode', (WidgetTester tester) async {
Brightness brightness = Brightness.light;
late StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return MediaQuery(
data: MediaQueryData(platformBrightness: brightness),
child: const CupertinoScrollbar(
child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
),
);
},
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, 10.0));
await tester.pump();
// Scrollbar fully showing
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
setState(() { brightness = Brightness.dark; });
await tester.pump();
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.darkColor,
));
});
testWidgets('Scrollbar thumb can be dragged with long press', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: const CupertinoScrollbar(
child: SingleChildScrollView(child: SizedBox(width: 4000.0, height: 4000.0)),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll down by swiping up.
await scrollGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls += 1;
}
return null;
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 50.0));
await tester.pump(kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb down to scroll down.
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration);
});
testWidgets('Scrollbar thumb can be dragged with long press - reverse', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: const CupertinoScrollbar(
child: SingleChildScrollView(
reverse: true,
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll up by swiping down.
await scrollGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls += 1;
}
return null;
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 550.0));
await tester.pump(kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb up to scroll up.
await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration);
});
testWidgets('Scrollbar changes thickness and radius when dragged', (WidgetTester tester) async {
const double thickness = 20;
const double thicknessWhileDragging = 40;
const double radius = 10;
const double radiusWhileDragging = 20;
const double inset = 3;
const double scaleFactor = 2;
final Size screenSize = tester.view.physicalSize / tester.view.devicePixelRatio;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: scrollController,
child: CupertinoScrollbar(
thickness: thickness,
thicknessWhileDragging: thicknessWhileDragging,
radius: const Radius.circular(radius),
radiusWhileDragging: const Radius.circular(radiusWhileDragging),
child: SingleChildScrollView(
child: SizedBox(
width: screenSize.width * scaleFactor,
height: screenSize.height * scaleFactor,
),
),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit to cause the scrollbar thumb to be shown;
// undo the scroll to put the thumb back at the top.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await scrollGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await scrollGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pump();
await scrollGesture.up();
await tester.pump();
// Long press on the scrollbar thumb and expect it to grow
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(780.0, 50.0));
await tester.pump(kLongPressDuration);
expect(find.byType(CupertinoScrollbar), paints..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTWH(
screenSize.width - inset - thickness,
inset,
thickness,
(screenSize.height - 2 * inset) / scaleFactor,
),
const Radius.circular(radius),
),
));
await tester.pump(kScrollbarResizeDuration ~/ 2);
const double midpointThickness = (thickness + thicknessWhileDragging) / 2;
const double midpointRadius = (radius + radiusWhileDragging) / 2;
expect(find.byType(CupertinoScrollbar), paints..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTWH(
screenSize.width - inset - midpointThickness,
inset,
midpointThickness,
(screenSize.height - 2 * inset) / scaleFactor,
),
const Radius.circular(midpointRadius),
),
));
await tester.pump(kScrollbarResizeDuration ~/ 2);
expect(find.byType(CupertinoScrollbar), paints..rrect(
rrect: RRect.fromRectAndRadius(
Rect.fromLTWH(
screenSize.width - inset - thicknessWhileDragging,
inset,
thicknessWhileDragging,
(screenSize.height - 2 * inset) / scaleFactor,
),
const Radius.circular(radiusWhileDragging),
),
));
// Let the thumb fade out so all timers have resolved.
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration);
});
testWidgets('When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async {
Widget viewWithScroll() {
return const Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(),
child: CupertinoScrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
},
);
testWidgets('When thumbVisibility is true, must pass a controller or find PrimaryScrollController that is attached to a scroll view', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: controller,
thumbVisibility: true,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
final FlutterExceptionHandler? handler = FlutterError.onError;
FlutterErrorDetails? error;
FlutterError.onError = (FlutterErrorDetails details) {
error = details;
};
await tester.pumpWidget(viewWithScroll());
expect(error, isNotNull);
FlutterError.onError = handler;
},
);
testWidgets('When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async {
Widget viewWithScroll() {
return const Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData(),
child: CupertinoScrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
},
);
testWidgets('When thumbVisibility is true, must pass a controller or find PrimaryScrollController that is attached to a scroll view', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: controller,
thumbVisibility: true,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
);
}
final FlutterExceptionHandler? handler = FlutterError.onError;
FlutterErrorDetails? error;
FlutterError.onError = (FlutterErrorDetails details) {
error = details;
};
await tester.pumpWidget(viewWithScroll());
expect(error, isNotNull);
FlutterError.onError = handler;
},
);
testWidgets('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: Builder(
builder: (BuildContext context) {
return const CupertinoScrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
primary: true,
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
);
},
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rect());
},
);
testWidgets('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: CupertinoScrollbar(
thumbVisibility: true,
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
// The scrollbar measures its size on the first frame
// and renders starting in the second,
//
// so pumpAndSettle a frame to allow it to appear.
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
});
testWidgets('On first render with thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: Builder(
builder: (BuildContext context) {
return const CupertinoScrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
primary: true,
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
);
},
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rect());
},
);
testWidgets('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: CupertinoScrollbar(
thumbVisibility: true,
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
// The scrollbar measures its size on the first frame
// and renders starting in the second,
//
// so pumpAndSettle a frame to allow it to appear.
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
});
testWidgets('On first render with thumbVisibility: false, the thumb is hidden', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
Widget viewWithScroll() {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: CupertinoScrollbar(
controller: controller,
child: const SingleChildScrollView(
child: SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
),
),
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rect()));
});
testWidgets('With thumbVisibility: true, fling a scroll. While it is still scrolling, set thumbVisibility: false. The thumb should not fade out until the scrolling stops.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool thumbVisibility = true;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
thumbVisibility: thumbVisibility,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
thumbVisibility = !thumbVisibility;
});
},
child: const Text('change thumbVisibility'),
),
),
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
},
);
testWidgets(
'With thumbVisibility: false, set thumbVisibility: true. The thumb should be always shown directly',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool thumbVisibility = false;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
thumbVisibility: thumbVisibility,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
thumbVisibility = !thumbVisibility;
});
},
child: const Text('change thumbVisibility'),
),
),
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
},
);
testWidgets(
'With thumbVisibility: false, fling a scroll. While it is still scrolling, set thumbVisibility: true. '
'The thumb should not fade even after the scrolling stops',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool thumbVisibility = false;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
thumbVisibility: thumbVisibility,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
thumbVisibility = !thumbVisibility;
});
},
child: const Text('change thumbVisibility'),
),
),
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
await tester.fling(
find.byType(SingleChildScrollView),
const Offset(0.0, -10.0),
10,
);
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pump();
expect(find.byType(CupertinoScrollbar), paints..rrect());
// Wait for the timer delay to expire.
await tester.pump(const Duration(milliseconds: 600)); // kScrollbarTimeToFade
await tester.pumpAndSettle();
// Scrollbar thumb is showing after scroll finishes and timer ends.
expect(find.byType(CupertinoScrollbar), paints..rrect());
},
);
testWidgets(
'Toggling thumbVisibility while not scrolling fades the thumb in/out. '
'This works even when you have never scrolled at all yet',
(WidgetTester tester) async {
final ScrollController controller = ScrollController();
bool thumbVisibility = true;
Widget viewWithScroll() {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Stack(
children: <Widget>[
CupertinoScrollbar(
thumbVisibility: thumbVisibility,
controller: controller,
child: SingleChildScrollView(
controller: controller,
child: const SizedBox(
width: 4000.0,
height: 4000.0,
),
),
),
Positioned(
bottom: 10,
child: CupertinoButton(
onPressed: () {
setState(() {
thumbVisibility = !thumbVisibility;
});
},
child: const Text('change thumbVisibility'),
),
),
],
),
),
);
},
);
}
await tester.pumpWidget(viewWithScroll());
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), paints..rrect());
await tester.tap(find.byType(CupertinoButton));
await tester.pumpAndSettle();
expect(find.byType(CupertinoScrollbar), isNot(paints..rrect()));
},
);
testWidgets('Scrollbar thumb can be dragged with long press - horizontal axis', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll right by swiping left.
await scrollGesture.moveBy(const Offset(-scrollAmount, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls += 1;
}
return null;
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(50.0, 596.0));
await tester.pump(kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb down to scroll back to the left.
await dragScrollbarGesture.moveBy(const Offset(scrollAmount, 0.0));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration);
});
testWidgets('Scrollbar thumb can be dragged with long press - horizontal axis, reverse', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: scrollController,
child: SingleChildScrollView(
reverse: true,
controller: scrollController,
scrollDirection: Axis.horizontal,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
expect(scrollController.offset, 0.0);
// Scroll a bit.
const double scrollAmount = 10.0;
final TestGesture scrollGesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
// Scroll right by swiping right.
await scrollGesture.moveBy(const Offset(scrollAmount, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
// Scrollbar thumb is fully showing and scroll offset has moved by
// scrollAmount.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
expect(scrollController.offset, scrollAmount);
await scrollGesture.up();
await tester.pump();
int hapticFeedbackCalls = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
hapticFeedbackCalls += 1;
}
return null;
});
// Long press on the scrollbar thumb and expect a vibration after it resizes.
expect(hapticFeedbackCalls, 0);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(750.0, 596.0));
await tester.pump(kLongPressDuration);
expect(hapticFeedbackCalls, 0);
await tester.pump(kScrollbarResizeDuration);
// Allow the haptic feedback some slack.
await tester.pump(const Duration(milliseconds: 1));
expect(hapticFeedbackCalls, 1);
// Drag the thumb to scroll back to the right.
await dragScrollbarGesture.moveBy(const Offset(-scrollAmount, 0.0));
await tester.pump(const Duration(milliseconds: 100));
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, greaterThan(scrollAmount * 2));
// The scrollbar thumb is still fully visible.
expect(find.byType(CupertinoScrollbar), paints..rrect(
color: _kScrollbarColor.color,
));
// Let the thumb fade out so all timers have resolved.
await tester.pump(kScrollbarTimeToFade);
await tester.pump(kScrollbarFadeDuration);
});
testWidgets('Tapping the track area pages the Scroll View', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
thumbVisibility: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 1000.0, height: 1000.0),
),
),
),
),
);
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
color: _kScrollbarColor.color,
rrect: RRect.fromLTRBR(794.0, 3.0, 797.0, 359.4, const Radius.circular(1.5)),
),
);
// Tap on the track area below the thumb.
await tester.tapAt(const Offset(796.0, 550.0));
await tester.pumpAndSettle();
expect(scrollController.offset, 400.0);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
color: _kScrollbarColor.color,
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 240.6, 797.0, 597.0),
const Radius.circular(1.5),
),
),
);
// Tap on the track area above the thumb.
await tester.tapAt(const Offset(796.0, 50.0));
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
color: _kScrollbarColor.color,
rrect: RRect.fromLTRBR(794.0, 3.0, 797.0, 359.4, const Radius.circular(1.5)),
),
);
});
testWidgets('Throw if interactive with the bar when no position attached', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CupertinoScrollbar(
controller: scrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(
height: 1000.0,
width: 1000.0,
),
),
),
),
),
);
await tester.pumpAndSettle();
final ScrollPosition position = scrollController.position;
scrollController.detach(position);
final FlutterExceptionHandler? handler = FlutterError.onError;
FlutterErrorDetails? error;
FlutterError.onError = (FlutterErrorDetails details) {
error = details;
};
// long press the thumb
await tester.startGesture(const Offset(796.0, 50.0));
await tester.pump(kLongPressDuration);
expect(error, isNotNull);
scrollController.attach(position);
FlutterError.onError = handler;
});
testWidgets('Interactive scrollbars should have a valid scroll controller', (WidgetTester tester) async {
final ScrollController primaryScrollController = ScrollController();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: primaryScrollController,
child: CupertinoScrollbar(
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(
height: 1000.0,
width: 1000.0,
),
),
),
),
),
),
);
await tester.pumpAndSettle();
AssertionError? exception = tester.takeException() as AssertionError?;
// The scrollbar is not visible and cannot be interacted with, so no assertion.
expect(exception, isNull);
// Scroll to trigger the scrollbar to come into view.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(SingleChildScrollView)));
await gesture.moveBy(const Offset(0.0, -20.0));
exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
expect(
exception.message,
contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
);
});
testWidgets('Simultaneous dragging and pointer scrolling does not cause a crash', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/70105
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
CupertinoApp(
home: PrimaryScrollController(
controller: scrollController,
child: CupertinoScrollbar(
thumbVisibility: true,
controller: scrollController,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
const double scrollAmount = 10.0;
await tester.pumpAndSettle();
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 3.0, 797.0, 92.1),
const Radius.circular(1.5),
),
color: _kScrollbarColor.color,
),
);
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(796.0, 50.0));
await tester.pump(kLongPressDuration);
await tester.pump(kScrollbarResizeDuration);
// Drag the thumb down to scroll down.
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
expect(scrollController.offset, greaterThan(10.0));
final double previousOffset = scrollController.offset;
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(789.0, 13.0, 797.0, 102.1),
const Radius.circular(4.0),
),
color: _kScrollbarColor.color,
),
);
// Execute a pointer scroll while dragging (drag gesture has not come up yet)
final TestPointer pointer = TestPointer(1, ui.PointerDeviceKind.mouse);
pointer.hover(const Offset(793.0, 15.0));
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, 20.0)));
await tester.pumpAndSettle();
if (!kIsWeb) {
// Scrolling while holding the drag on the scrollbar and still hovered over
// the scrollbar should not have changed the scroll offset.
expect(pointer.location, const Offset(793.0, 15.0));
expect(scrollController.offset, previousOffset);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(789.0, 13.0, 797.0, 102.1),
const Radius.circular(4.0),
),
color: _kScrollbarColor.color,
),
);
} else {
expect(pointer.location, const Offset(793.0, 15.0));
expect(scrollController.offset, previousOffset + 20.0);
}
// Drag is still being held, move pointer to be hovering over another area
// of the scrollable (not over the scrollbar) and execute another pointer scroll
pointer.hover(tester.getCenter(find.byType(SingleChildScrollView)));
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, -90.0)));
await tester.pumpAndSettle();
// Scrolling while holding the drag on the scrollbar changed the offset
expect(pointer.location, const Offset(400.0, 300.0));
expect(scrollController.offset, 0.0);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(789.0, 3.0, 797.0, 92.1),
const Radius.circular(4.0),
),
color: _kScrollbarColor.color,
),
);
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(
find.byType(CupertinoScrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 3.0, 797.0, 92.1),
const Radius.circular(1.5),
),
color: _kScrollbarColor.color,
),
);
});
testWidgets('CupertinoScrollbar scrollOrientation works correctly', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
CupertinoApp(
home: PrimaryScrollController(
controller: scrollController,
child: CupertinoScrollbar(
thumbVisibility: true,
controller: scrollController,
scrollbarOrientation: ScrollbarOrientation.left,
child: const SingleChildScrollView(
child: SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byType(CupertinoScrollbar),
paints
..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 9.0, 600.0),
)
..line(
p1: const Offset(9.0, 0.0),
p2: const Offset(9.0, 600.0),
strokeWidth: 1.0,
)
..rrect(
rrect: RRect.fromRectAndRadius(const Rect.fromLTRB(3.0, 3.0, 6.0, 92.1), const Radius.circular(1.5)),
color: _kScrollbarColor.color,
),
);
});
}