| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| class TestCanvas implements Canvas { |
| final List<Invocation> invocations = <Invocation>[]; |
| |
| @override |
| void noSuchMethod(Invocation invocation) { |
| invocations.add(invocation); |
| } |
| } |
| |
| Widget _buildBoilerplate({ |
| TextDirection textDirection = TextDirection.ltr, |
| EdgeInsets padding = EdgeInsets.zero, |
| required Widget child, |
| }) { |
| return Directionality( |
| textDirection: textDirection, |
| child: MediaQuery( |
| data: MediaQueryData(padding: padding), |
| child: child, |
| ), |
| ); |
| } |
| |
| void main() { |
| testWidgets("Scrollbar doesn't show when tapping list", (WidgetTester tester) async { |
| await tester.pumpWidget( |
| _buildBoilerplate( |
| child: Center( |
| child: Container( |
| decoration: BoxDecoration( |
| border: Border.all(color: const Color(0xFFFFFF00)) |
| ), |
| height: 200.0, |
| width: 300.0, |
| child: Scrollbar( |
| child: ListView( |
| children: <Widget>[ |
| Container(height: 40.0, child: const Text('0')), |
| Container(height: 40.0, child: const Text('1')), |
| Container(height: 40.0, child: const Text('2')), |
| Container(height: 40.0, child: const Text('3')), |
| Container(height: 40.0, child: const Text('4')), |
| Container(height: 40.0, child: const Text('5')), |
| Container(height: 40.0, child: const Text('6')), |
| Container(height: 40.0, child: const Text('7')), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.'); |
| await tester.tap(find.byType(ListView)); |
| SchedulerBinding.instance!.debugAssertNoTransientCallbacks('Tapping a block with a scrollbar triggered an animation.'); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.drag(find.byType(ListView), const Offset(0.0, -10.0)); |
| expect(SchedulerBinding.instance!.transientCallbackCount, greaterThan(0)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| }); |
| |
| testWidgets('ScrollbarPainter does not divide by zero', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| _buildBoilerplate(child: Container( |
| height: 200.0, |
| width: 300.0, |
| child: Scrollbar( |
| child: ListView( |
| children: <Widget>[ |
| Container(height: 40.0, child: const Text('0')), |
| ], |
| ), |
| ), |
| )), |
| ); |
| |
| final CustomPaint custom = tester.widget(find.descendant( |
| of: find.byType(Scrollbar), |
| matching: find.byType(CustomPaint), |
| ).first); |
| final dynamic scrollPainter = custom.foregroundPainter; |
| // Dragging makes the scrollbar first appear. |
| await tester.drag(find.text('0'), const Offset(0.0, -10.0)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| await tester.pump(const Duration(milliseconds: 200)); |
| |
| final ScrollMetrics metrics = FixedScrollMetrics( |
| minScrollExtent: 0.0, |
| maxScrollExtent: 0.0, |
| pixels: 0.0, |
| viewportDimension: 100.0, |
| axisDirection: AxisDirection.down, |
| ); |
| scrollPainter.update(metrics, AxisDirection.down); |
| |
| final TestCanvas canvas = TestCanvas(); |
| scrollPainter.paint(canvas, const Size(10.0, 100.0)); |
| |
| // Scrollbar is not supposed to draw anything if there isn't enough content. |
| expect(canvas.invocations.isEmpty, isTrue); |
| }); |
| |
| testWidgets('Adaptive scrollbar', (WidgetTester tester) async { |
| Widget viewWithScroll(TargetPlatform platform) { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData( |
| platform: platform |
| ), |
| child: const Scrollbar( |
| child: SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll(TargetPlatform.android)); |
| await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0)); |
| await tester.pump(); |
| // Scrollbar fully showing |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect(find.byType(Scrollbar), paints..rect()); |
| |
| await tester.pumpWidget(viewWithScroll(TargetPlatform.iOS)); |
| final TestGesture gesture = await tester.startGesture( |
| tester.getCenter(find.byType(SingleChildScrollView)) |
| ); |
| await gesture.moveBy(const Offset(0.0, -10.0)); |
| await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(find.byType(Scrollbar), paints..rrect()); |
| expect(find.byType(CupertinoScrollbar), paints..rrect()); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| await tester.pumpWidget(viewWithScroll(TargetPlatform.macOS)); |
| await gesture.down( |
| tester.getCenter(find.byType(SingleChildScrollView)), |
| ); |
| await gesture.moveBy(const Offset(0.0, -10.0)); |
| await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(find.byType(Scrollbar), paints..rrect()); |
| expect(find.byType(CupertinoScrollbar), paints..rrect()); |
| }); |
| |
| testWidgets('Scrollbar passes controller to CupertinoScrollbar', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll(TargetPlatform? platform) { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData( |
| platform: platform |
| ), |
| child: Scrollbar( |
| controller: controller, |
| child: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll(debugDefaultTargetPlatformOverride)); |
| final TestGesture gesture = await tester.startGesture( |
| tester.getCenter(find.byType(SingleChildScrollView)) |
| ); |
| await gesture.moveBy(const Offset(0.0, -10.0)); |
| await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -10.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 200)); |
| expect(find.byType(CupertinoScrollbar), paints..rrect()); |
| final CupertinoScrollbar scrollbar = find.byType(CupertinoScrollbar).evaluate().first.widget as CupertinoScrollbar; |
| expect(scrollbar.controller, isNotNull); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('When isAlwaysShown is true, must pass a controller', |
| (WidgetTester tester) async { |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| isAlwaysShown: true, |
| child: const SingleChildScrollView( |
| child: SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| expect(() async { |
| await tester.pumpWidget(viewWithScroll()); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('When isAlwaysShown is true, must pass a controller that is attached to a scroll view', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| isAlwaysShown: true, |
| controller: controller, |
| child: const SingleChildScrollView( |
| child: SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| final dynamic exception = tester.takeException(); |
| expect(exception, isAssertionError); |
| }); |
| |
| testWidgets('On first render with isAlwaysShown: true, the thumb shows', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| isAlwaysShown: true, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(Scrollbar), paints..rect()); |
| }); |
| |
| testWidgets('On first render with isAlwaysShown: false, the thumb is hidden', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| isAlwaysShown: false, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(Scrollbar), isNot(paints..rect())); |
| }); |
| |
| testWidgets( |
| 'With isAlwaysShown: true, fling a scroll. While it is still scrolling, set isAlwaysShown: false. The thumb should not fade out until the scrolling stops.', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| bool isAlwaysShown = true; |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Theme( |
| data: ThemeData(), |
| child: Scaffold( |
| floatingActionButton: FloatingActionButton( |
| child: const Icon(Icons.threed_rotation), |
| onPressed: () { |
| setState(() { |
| isAlwaysShown = !isAlwaysShown; |
| }); |
| }, |
| ), |
| body: Scrollbar( |
| isAlwaysShown: isAlwaysShown, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| await tester.fling( |
| find.byType(SingleChildScrollView), |
| const Offset(0.0, -10.0), |
| 10, |
| ); |
| expect(find.byType(Scrollbar), paints..rect()); |
| |
| await tester.tap(find.byType(FloatingActionButton)); |
| await tester.pumpAndSettle(); |
| // Scrollbar is not showing after scroll finishes |
| expect(find.byType(Scrollbar), isNot(paints..rect())); |
| }); |
| |
| testWidgets( |
| 'With isAlwaysShown: false, set isAlwaysShown: true. The thumb should be always shown directly', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| bool isAlwaysShown = false; |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Theme( |
| data: ThemeData(), |
| child: Scaffold( |
| floatingActionButton: FloatingActionButton( |
| child: const Icon(Icons.threed_rotation), |
| onPressed: () { |
| setState(() { |
| isAlwaysShown = !isAlwaysShown; |
| }); |
| }, |
| ), |
| body: Scrollbar( |
| isAlwaysShown: isAlwaysShown, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(Scrollbar), isNot(paints..rect())); |
| |
| await tester.tap(find.byType(FloatingActionButton)); |
| await tester.pumpAndSettle(); |
| // Scrollbar is not showing after scroll finishes |
| expect(find.byType(Scrollbar), paints..rect()); |
| }); |
| |
| testWidgets( |
| 'With isAlwaysShown: false, fling a scroll. While it is still scrolling, set isAlwaysShown: true. The thumb should not fade even after the scrolling stops', |
| (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| bool isAlwaysShown = false; |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Theme( |
| data: ThemeData(), |
| child: Scaffold( |
| floatingActionButton: FloatingActionButton( |
| child: const Icon(Icons.threed_rotation), |
| onPressed: () { |
| setState(() { |
| isAlwaysShown = !isAlwaysShown; |
| }); |
| }, |
| ), |
| body: Scrollbar( |
| isAlwaysShown: isAlwaysShown, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(Scrollbar), isNot(paints..rect())); |
| await tester.fling( |
| find.byType(SingleChildScrollView), |
| const Offset(0.0, -10.0), |
| 10, |
| ); |
| expect(find.byType(Scrollbar), paints..rect()); |
| |
| await tester.tap(find.byType(FloatingActionButton)); |
| await tester.pump(); |
| expect(find.byType(Scrollbar), paints..rect()); |
| |
| // 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(Scrollbar), paints..rect()); |
| }); |
| |
| testWidgets( |
| 'Toggling isAlwaysShown 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 isAlwaysShown = true; |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Theme( |
| data: ThemeData(), |
| child: Scaffold( |
| floatingActionButton: FloatingActionButton( |
| child: const Icon(Icons.threed_rotation), |
| onPressed: () { |
| setState(() { |
| isAlwaysShown = !isAlwaysShown; |
| }); |
| }, |
| ), |
| body: Scrollbar( |
| isAlwaysShown: isAlwaysShown, |
| controller: controller, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| final Finder materialScrollbar = find.byType(Scrollbar); |
| expect(materialScrollbar, paints..rect()); |
| |
| await tester.tap(find.byType(FloatingActionButton)); |
| await tester.pumpAndSettle(); |
| expect(materialScrollbar, isNot(paints..rect())); |
| }); |
| |
| testWidgets('Scrollbar respects thickness and radius', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll({Radius? radius}) { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| controller: controller, |
| thickness: 20, |
| radius: radius, |
| child: SingleChildScrollView( |
| controller: controller, |
| child: const SizedBox( |
| width: 1600.0, |
| height: 1200.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Scroll a bit to cause the scrollbar thumb to be shown; |
| // undo the scroll to put the thumb back at the top. |
| await tester.pumpWidget(viewWithScroll()); |
| 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 |
| expect(find.byType(Scrollbar), paints..rect( |
| rect: const Rect.fromLTWH(780, 0, 20, 300), |
| )); |
| await tester.pumpWidget(viewWithScroll(radius: const Radius.circular(10))); |
| expect(find.byType(Scrollbar), paints..rrect( |
| rrect: RRect.fromRectAndRadius(const Rect.fromLTWH(780, 0, 20, 300), const Radius.circular(10)), |
| )); |
| |
| await tester.pumpAndSettle(); |
| }); |
| } |