| // 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. |
| |
| // TODO(gspencergoog): Remove this tag once this test's state leaks/test |
| // dependencies have been fixed. |
| // https://github.com/flutter/flutter/issues/85160 |
| // Fails with "flutter test --test-randomize-ordering-seed=382757700" |
| @Tags(<String>['no-shuffle']) |
| |
| import 'dart:ui' as ui; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| const Duration _kScrollbarFadeDuration = Duration(milliseconds: 300); |
| const Duration _kScrollbarTimeToFade = Duration(milliseconds: 600); |
| const Color _kAndroidThumbIdleColor = Color(0xffbcbcbc); |
| const Rect _kAndroidTrackDimensions = Rect.fromLTRB(796.0, 0.0, 800.0, 600.0); |
| const Radius _kDefaultThumbRadius = Radius.circular(8.0); |
| const Color _kDefaultIdleThumbColor = Color(0x1a000000); |
| const Offset _kTrackBorderPoint1 = Offset(796.0, 0.0); |
| const Offset _kTrackBorderPoint2 = Offset(796.0, 600.0); |
| |
| Rect getStartingThumbRect({ required bool isAndroid }) { |
| return isAndroid |
| // On Android the thumb is slightly different. The thumb is only 4 pixels wide, |
| // and has no margin along the side of the viewport. |
| ? const Rect.fromLTRB(796.0, 0.0, 800.0, 90.0) |
| // The Material Design thumb is 8 pixels wide, with a 2 |
| // pixel margin to the right edge of the viewport. |
| : const Rect.fromLTRB(790.0, 0.0, 798.0, 90.0); |
| } |
| |
| 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: ScrollConfiguration( |
| behavior: const NoScrollbarBehavior(), |
| child: child, |
| ), |
| ), |
| ); |
| } |
| |
| class NoScrollbarBehavior extends MaterialScrollBehavior { |
| const NoScrollbarBehavior(); |
| |
| @override |
| Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => 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: const <Widget>[ |
| SizedBox(height: 40.0, child: Text('0')), |
| SizedBox(height: 40.0, child: Text('1')), |
| SizedBox(height: 40.0, child: Text('2')), |
| SizedBox(height: 40.0, child: Text('3')), |
| SizedBox(height: 40.0, child: Text('4')), |
| SizedBox(height: 40.0, child: Text('5')), |
| SizedBox(height: 40.0, child: Text('6')), |
| SizedBox(height: 40.0, child: 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: SizedBox( |
| height: 200.0, |
| width: 300.0, |
| child: Scrollbar( |
| child: ListView( |
| children: const <Widget>[ |
| SizedBox(height: 40.0, child: Text('0')), |
| ], |
| ), |
| ), |
| )), |
| ); |
| |
| final CustomPaint custom = tester.widget(find.descendant( |
| of: find.byType(Scrollbar), |
| matching: find.byType(CustomPaint), |
| ).first); |
| final ScrollbarPainter? scrollPainter = custom.foregroundPainter as ScrollbarPainter?; |
| // 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('When thumbVisibility is true, must pass a controller or find PrimaryScrollController', (WidgetTester tester) async { |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: const Scrollbar( |
| 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 that is attached to a scroll view or find PrimaryScrollController', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| thumbVisibility: true, |
| controller: controller, |
| child: const SingleChildScrollView( |
| child: SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| final AssertionError exception = tester.takeException() as AssertionError; |
| expect(exception, isAssertionError); |
| }); |
| |
| testWidgets('On first render with thumbVisibility: true, the thumb shows', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: Scrollbar( |
| thumbVisibility: 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 thumbVisibility: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: PrimaryScrollController( |
| controller: controller, |
| child: Builder( |
| builder: (BuildContext context) { |
| return const Scrollbar( |
| thumbVisibility: true, |
| child: SingleChildScrollView( |
| primary: true, |
| child: SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(viewWithScroll()); |
| await tester.pumpAndSettle(); |
| expect(find.byType(Scrollbar), paints..rect()); |
| }); |
| |
| testWidgets( |
| 'When isAlwaysShown is true, must pass a controller or find PrimaryScrollController', |
| (WidgetTester tester) async { |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: const Scrollbar( |
| isAlwaysShown: 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 isAlwaysShown is true, must pass a controller that is attached to a scroll view or find PrimaryScrollController', |
| (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 AssertionError exception = tester.takeException() as AssertionError; |
| 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: true, the thumb shows with PrimaryScrollController', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(); |
| Widget viewWithScroll() { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData(), |
| child: PrimaryScrollController( |
| controller: controller, |
| child: Builder( |
| builder: (BuildContext context) { |
| return const Scrollbar( |
| isAlwaysShown: true, |
| child: SingleChildScrollView( |
| primary: true, |
| child: 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.fromLTRB(780.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(780.0, 0.0), |
| p2: const Offset(780.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(780.0, 0.0, 800.0, 300.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| await tester.pumpWidget(viewWithScroll(radius: const Radius.circular(10))); |
| expect(find.byType(Scrollbar), paints..rrect( |
| rrect: RRect.fromRectAndRadius(const Rect.fromLTRB(780, 0.0, 800.0, 300.0), const Radius.circular(10)), |
| )); |
| |
| await tester.pumpAndSettle(); |
| }); |
| |
| 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: Scrollbar( |
| interactive: true, |
| isAlwaysShown: 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(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 360.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| // 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(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 240.0, 800.0, 600.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| // 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(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 360.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar never goes away until finger lift', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Scrollbar( |
| 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, -20.0)); |
| await tester.pump(); |
| // Scrollbar fully showing |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 3.0, 800.0, 93.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| await tester.pump(const Duration(seconds: 3)); |
| await tester.pump(const Duration(seconds: 3)); |
| // Still there. |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 3.0, 800.0, 93.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| await gesture.up(); |
| await tester.pump(_kScrollbarTimeToFade); |
| await tester.pump(_kScrollbarFadeDuration * 0.5); |
| |
| // Opacity going down now. |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 3.0, 800.0, 93.0), |
| color: const Color(0xc6bcbcbc), |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar thumb can be dragged', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: PrimaryScrollController( |
| controller: scrollController, |
| child: Scrollbar( |
| interactive: true, |
| isAlwaysShown: true, |
| controller: scrollController, |
| child: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: getStartingThumbRect(isAndroid: true), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| // Drag the thumb down to scroll down. |
| const double scrollAmount = 10.0; |
| final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0)); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: getStartingThumbRect(isAndroid: true), |
| // Drag color |
| color: const Color(0x99000000), |
| ), |
| ); |
| |
| await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount)); |
| await tester.pumpAndSettle(); |
| await dragScrollbarGesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // The view has scrolled more than it would have by a swipe pointer of the |
| // same distance. |
| expect(scrollController.offset, greaterThan(scrollAmount * 2)); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 10.0, 800.0, 100.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar thumb color completes a hover animation', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(isAlwaysShown: true)), |
| home: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(const Offset(794.0, 5.0)); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| // Hover color |
| color: const Color(0x80000000), |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.linux, |
| TargetPlatform.macOS, |
| TargetPlatform.windows, |
| }), |
| ); |
| |
| testWidgets('Hover animation is not triggered by tap gestures', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(scrollbarTheme: const ScrollbarThemeData( |
| isAlwaysShown: true, |
| showTrackOnHover: true, |
| )), |
| home: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| await tester.tapAt(const Offset(794.0, 5.0)); |
| await tester.pumpAndSettle(); |
| |
| // Tapping triggers a hover enter event. In this case, the Scrollbar should |
| // be unchanged since it ignores hover events that aren't from a mouse. |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| // Now trigger hover with a mouse. |
| final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(const Offset(794.0, 5.0)); |
| await tester.pump(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0), |
| color: const Color(0x08000000), |
| ) |
| ..line( |
| p1: const Offset(784.0, 0.0), |
| p2: const Offset(784.0, 600.0), |
| strokeWidth: 1.0, |
| color: _kDefaultIdleThumbColor, |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| // Scrollbar thumb is larger |
| const Rect.fromLTRB(786.0, 0.0, 798.0, 90.0), |
| _kDefaultThumbRadius, |
| ), |
| // Hover color |
| color: const Color(0x80000000), |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux }), |
| ); |
| |
| testWidgets('ScrollbarThemeData.thickness replaces hoverThickness', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(scrollbarTheme: ScrollbarThemeData( |
| thumbVisibility: MaterialStateProperty.resolveWith((Set<MaterialState> states) => true), |
| trackVisibility: MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| return states.contains(MaterialState.hovered); |
| }), |
| thickness: MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.hovered)) { |
| return 40.0; |
| } |
| // Default thickness |
| return 8.0; |
| }), |
| )), |
| home: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(const Offset(794.0, 5.0)); |
| await tester.pump(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(756.0, 0.0, 800.0, 600.0), |
| color: const Color(0x08000000), |
| ) |
| ..line( |
| p1: const Offset(756.0, 0.0), |
| p2: const Offset(756.0, 600.0), |
| strokeWidth: 1.0, |
| color: _kDefaultIdleThumbColor, |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| // Scrollbar thumb is larger |
| const Rect.fromLTRB(758.0, 0.0, 798.0, 90.0), |
| _kDefaultThumbRadius, |
| ), |
| // Hover color |
| color: const Color(0x80000000), |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.linux, |
| TargetPlatform.macOS, |
| TargetPlatform.windows, |
| }), |
| ); |
| |
| testWidgets('ScrollbarThemeData.trackVisibility replaces showTrackOnHover', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(scrollbarTheme: ScrollbarThemeData( |
| isAlwaysShown: true, |
| trackVisibility: MaterialStateProperty.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.hovered)) { |
| return true; |
| } |
| return false; |
| }), |
| )), |
| home: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(const Offset(794.0, 5.0)); |
| await tester.pump(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0), |
| color: const Color(0x08000000), |
| ) |
| ..line( |
| p1: const Offset(784.0, 0.0), |
| p2: const Offset(784.0, 600.0), |
| strokeWidth: 1.0, |
| color: _kDefaultIdleThumbColor, |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| // Scrollbar thumb is larger |
| const Rect.fromLTRB(786.0, 0.0, 798.0, 90.0), |
| _kDefaultThumbRadius, |
| ), |
| // Hover color |
| color: const Color(0x80000000), |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.linux, |
| TargetPlatform.macOS, |
| TargetPlatform.windows, |
| }), |
| ); |
| |
| testWidgets('Scrollbar showTrackOnHover', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(scrollbarTheme: const ScrollbarThemeData( |
| isAlwaysShown: true, |
| showTrackOnHover: true, |
| )), |
| home: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect( |
| find.byType(Scrollbar), |
| paints..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(const Offset(794.0, 5.0)); |
| await tester.pump(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0), |
| color: const Color(0x08000000), |
| ) |
| ..line( |
| p1: const Offset(784.0, 0.0), |
| p2: const Offset(784.0, 600.0), |
| strokeWidth: 1.0, |
| color: _kDefaultIdleThumbColor, |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| // Scrollbar thumb is larger |
| const Rect.fromLTRB(786.0, 0.0, 798.0, 90.0), |
| _kDefaultThumbRadius, |
| ), |
| // Hover color |
| color: const Color(0x80000000), |
| ), |
| ); |
| }, |
| variant: const TargetPlatformVariant(<TargetPlatform>{ |
| TargetPlatform.linux, |
| TargetPlatform.macOS, |
| TargetPlatform.windows, |
| }), |
| ); |
| |
| 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(); |
| }); |
| |
| 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: SingleChildScrollView( |
| controller: controller, |
| child: const 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 = tester.widget<CupertinoScrollbar>(find.byType(CupertinoScrollbar)); |
| expect(scrollbar.controller, isNotNull); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); |
| |
| testWidgets("Scrollbar doesn't show when scroll the inner scrollable widget", (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(); |
| final GlobalKey key2 = GlobalKey(); |
| final GlobalKey outerKey = GlobalKey(); |
| final GlobalKey innerKey = GlobalKey(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: ScrollConfiguration( |
| behavior: const NoScrollbarBehavior(), |
| child: Scrollbar( |
| key: key2, |
| child: SingleChildScrollView( |
| key: outerKey, |
| child: SizedBox( |
| height: 1000.0, |
| width: double.infinity, |
| child: Column( |
| children: <Widget>[ |
| Scrollbar( |
| key: key1, |
| child: SizedBox( |
| height: 300.0, |
| width: double.infinity, |
| child: SingleChildScrollView( |
| key: innerKey, |
| child: const SizedBox( |
| key: Key('Inner scrollable'), |
| height: 1000.0, |
| width: double.infinity, |
| ), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Drag the inner scrollable widget. |
| await tester.drag(find.byKey(innerKey), const Offset(0.0, -25.0)); |
| await tester.pump(); |
| // Scrollbar fully showing. |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect( |
| tester.renderObject(find.byKey(key2)), |
| paintsExactlyCountTimes(#drawRect, 2), // Each bar will call [drawRect] twice. |
| ); |
| |
| expect( |
| tester.renderObject(find.byKey(key1)), |
| paintsExactlyCountTimes(#drawRect, 2), |
| ); |
| }, variant: TargetPlatformVariant.all()); |
| |
| testWidgets('Scrollbar dragging can be disabled', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: PrimaryScrollController( |
| controller: scrollController, |
| child: Scrollbar( |
| interactive: false, |
| isAlwaysShown: true, |
| controller: scrollController, |
| child: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(788.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(788.0, 0.0), |
| p2: const Offset(788.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rrect( |
| rrect: RRect.fromRectAndRadius( |
| getStartingThumbRect(isAndroid: false), |
| _kDefaultThumbRadius, |
| ), |
| color: _kDefaultIdleThumbColor, |
| ), |
| ); |
| |
| // Try to drag the thumb down. |
| const double scrollAmount = 10.0; |
| final TestGesture dragScrollbarThumbGesture = await tester.startGesture(const Offset(797.0, 45.0)); |
| await tester.pumpAndSettle(); |
| await dragScrollbarThumbGesture.moveBy(const Offset(0.0, scrollAmount)); |
| await tester.pumpAndSettle(); |
| await dragScrollbarThumbGesture.up(); |
| await tester.pumpAndSettle(); |
| // Dragging on the thumb does not change the offset. |
| expect(scrollController.offset, 0.0); |
| |
| // Drag in the track area to validate pass through to scrollable. |
| final TestGesture dragPassThroughTrack = await tester.startGesture(const Offset(797.0, 250.0)); |
| await dragPassThroughTrack.moveBy(const Offset(0.0, -scrollAmount)); |
| await tester.pumpAndSettle(); |
| await dragPassThroughTrack.up(); |
| await tester.pumpAndSettle(); |
| // The scroll view received the drag. |
| expect(scrollController.offset, scrollAmount); |
| |
| // Tap on the track to validate the scroll view will not page. |
| await tester.tapAt(const Offset(797.0, 200.0)); |
| await tester.pumpAndSettle(); |
| // The offset should not have changed. |
| expect(scrollController.offset, scrollAmount); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia })); |
| |
| testWidgets('Scrollbar dragging is disabled by default on Android', (WidgetTester tester) async { |
| int tapCount = 0; |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: PrimaryScrollController( |
| controller: scrollController, |
| child: Scrollbar( |
| isAlwaysShown: true, |
| controller: scrollController, |
| child: SingleChildScrollView( |
| dragStartBehavior: DragStartBehavior.down, |
| child: GestureDetector( |
| behavior: HitTestBehavior.opaque, |
| onTap: () { |
| tapCount += 1; |
| }, |
| child: const SizedBox( |
| width: 4000.0, |
| height: 4000.0, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: getStartingThumbRect(isAndroid: true), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| // Try to drag the thumb down. |
| const double scrollAmount = 50.0; |
| await tester.dragFrom( |
| const Offset(797.0, 45.0), |
| const Offset(0.0, scrollAmount), |
| touchSlopY: 0.0, |
| ); |
| await tester.pumpAndSettle(); |
| // Dragging on the thumb does not change the offset. |
| expect(scrollController.offset, 0.0); |
| expect(tapCount, 0); |
| |
| // Try to drag up in the thumb area to validate pass through to scrollable. |
| await tester.dragFrom( |
| const Offset(797.0, 45.0), |
| const Offset(0.0, -scrollAmount), |
| ); |
| await tester.pumpAndSettle(); |
| // The scroll view received the drag. |
| expect(scrollController.offset, scrollAmount); |
| expect(tapCount, 0); |
| |
| // Drag in the track area to validate pass through to scrollable. |
| await tester.dragFrom( |
| const Offset(797.0, 45.0), |
| const Offset(0.0, -scrollAmount), |
| touchSlopY: 0.0, |
| ); |
| await tester.pumpAndSettle(); |
| // The scroll view received the drag. |
| expect(scrollController.offset, scrollAmount * 2); |
| expect(tapCount, 0); |
| |
| // Tap on the thumb to validate the scroll view receives a click. |
| await tester.tapAt(const Offset(797.0, 45.0)); |
| await tester.pumpAndSettle(); |
| expect(tapCount, 1); |
| |
| // Tap on the track to validate the scroll view will not page and receives a click. |
| await tester.tapAt(const Offset(797.0, 400.0)); |
| await tester.pumpAndSettle(); |
| // The offset should not have changed. |
| expect(scrollController.offset, scrollAmount * 2); |
| expect(tapCount, 2); |
| }); |
| |
| 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( |
| MaterialApp( |
| home: PrimaryScrollController( |
| controller: scrollController, |
| child: Scrollbar( |
| interactive: true, |
| isAlwaysShown: true, |
| controller: scrollController, |
| child: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0), |
| ), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: getStartingThumbRect(isAndroid: true), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| |
| // Drag the thumb down to scroll down. |
| const double scrollAmount = 10.0; |
| final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0)); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: _kAndroidTrackDimensions, |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: _kTrackBorderPoint1, |
| p2: _kTrackBorderPoint2, |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: getStartingThumbRect(isAndroid: true), |
| // Drag color |
| color: const Color(0x99000000), |
| ), |
| ); |
| |
| 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(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(796.0, 0.0), |
| p2: const Offset(796.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 10.0, 800.0, 100.0), |
| color: const Color(0x99000000), |
| ), |
| ); |
| |
| // 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(798.0, 15.0)); |
| await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, 20.0))); |
| await tester.pumpAndSettle(); |
| // 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(798.0, 15.0)); |
| expect(scrollController.offset, previousOffset); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(796.0, 0.0), |
| p2: const Offset(796.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 10.0, 800.0, 100.0), |
| color: const Color(0x99000000), |
| ), |
| ); |
| |
| // 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, -70.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(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(796.0, 0.0), |
| p2: const Offset(796.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 90.0), |
| color: const Color(0x99000000), |
| ), |
| ); |
| |
| await dragScrollbarGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(scrollController.offset, 0.0); |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(796.0, 0.0), |
| p2: const Offset(796.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(796.0, 0.0, 800.0, 90.0), |
| color: const Color(0xffbcbcbc), |
| ), |
| ); |
| }); |
| |
| testWidgets('Scrollbar.isAlwaysShown triggers assertion when multiple ScrollPositions are attached.', (WidgetTester tester) async { |
| Widget getTabContent({ ScrollController? scrollController }) { |
| return Scrollbar( |
| isAlwaysShown: true, |
| controller: scrollController, |
| child: ListView.builder( |
| controller: scrollController, |
| itemCount: 200, |
| itemBuilder: (BuildContext context, int index) => const Text('Test'), |
| ), |
| ); |
| } |
| |
| Widget buildApp({ |
| required String id, |
| ScrollController? scrollController, |
| }) { |
| return MaterialApp( |
| key: ValueKey<String>(id), |
| home: DefaultTabController( |
| length: 2, |
| child: Scaffold( |
| body: TabBarView( |
| children: <Widget>[ |
| getTabContent(scrollController: scrollController), |
| getTabContent(scrollController: scrollController), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // Asserts when using the PrimaryScrollController. |
| await tester.pumpWidget(buildApp(id: 'PrimaryScrollController')); |
| |
| // Swipe to the second tab, resulting in two attached ScrollPositions during |
| // the transition. |
| await tester.drag(find.text('Test').first, const Offset(-100.0, 0.0)); |
| await tester.pump(); |
| |
| FlutterError error = tester.takeException() as FlutterError; |
| expect( |
| error.message, |
| contains('The PrimaryScrollController is currently attached to more than one ScrollPosition.'), |
| ); |
| |
| // Asserts when using the ScrollController provided by the user. |
| final ScrollController scrollController = ScrollController(); |
| await tester.pumpWidget( |
| buildApp( |
| id: 'Provided ScrollController', |
| scrollController: scrollController, |
| ), |
| ); |
| |
| // Swipe to the second tab, resulting in two attached ScrollPositions during |
| // the transition. |
| await tester.drag(find.text('Test').first, const Offset(-100.0, 0.0)); |
| await tester.pump(); |
| error = tester.takeException() as FlutterError; |
| expect( |
| error.message, |
| contains('The provided ScrollController is currently attached to more than one ScrollPosition.'), |
| ); |
| }); |
| |
| testWidgets('Scrollbar scrollOrientation works correctly', (WidgetTester tester) async { |
| final ScrollController scrollController = ScrollController(); |
| |
| Widget buildScrollWithOrientation(ScrollbarOrientation orientation) { |
| return _buildBoilerplate( |
| child: Theme( |
| data: ThemeData( |
| platform: TargetPlatform.android, |
| ), |
| child: PrimaryScrollController( |
| controller: scrollController, |
| child: Scrollbar( |
| interactive: true, |
| isAlwaysShown: true, |
| scrollbarOrientation: orientation, |
| controller: scrollController, |
| child: const SingleChildScrollView( |
| child: SizedBox(width: 4000.0, height: 4000.0) |
| ), |
| ), |
| ), |
| ) |
| ); |
| } |
| |
| await tester.pumpWidget(buildScrollWithOrientation(ScrollbarOrientation.left)); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| find.byType(Scrollbar), |
| paints |
| ..rect( |
| rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 600.0), |
| color: Colors.transparent, |
| ) |
| ..line( |
| p1: const Offset(4.0, 0.0), |
| p2: const Offset(4.0, 600.0), |
| strokeWidth: 1.0, |
| color: Colors.transparent, |
| ) |
| ..rect( |
| rect: const Rect.fromLTRB(0.0, 0.0, 4.0, 90.0), |
| color: _kAndroidThumbIdleColor, |
| ), |
| ); |
| }); |
| } |