Unverified Commit 40d0c69f authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Make scrollbar assertions less aggressive (#84809)

parent 604c59e5
...@@ -665,25 +665,114 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -665,25 +665,114 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// the [PrimaryScrollController] is being used by that Scrollable widget. /// the [PrimaryScrollController] is being used by that Scrollable widget.
/// ///
/// If the scrollbar is wrapped around multiple [ScrollView]s, it only responds to /// If the scrollbar is wrapped around multiple [ScrollView]s, it only responds to
/// the nearest scrollView and shows the corresponding scrollbar thumb by default. /// the nearest ScrollView and shows the corresponding scrollbar thumb by default.
/// The [notificationPredicate] allows the ability to customize which /// The [notificationPredicate] allows the ability to customize which
/// [ScrollNotification]s the Scrollbar should listen to. /// [ScrollNotification]s the Scrollbar should listen to.
/// ///
/// Scrollbars are interactive and will also use the [PrimaryScrollController] if
/// a [controller] is not set. Scrollbar thumbs can be dragged along the main axis
/// of the [ScrollView] to change the [ScrollPosition]. Tapping along the track
/// exclusive of the thumb will trigger a [ScrollIncrementType.page] based on
/// the relative position to the thumb.
///
/// If the child [ScrollView] is infinitely long, the [RawScrollbar] will not be /// If the child [ScrollView] is infinitely long, the [RawScrollbar] will not be
/// painted. In this case, the scrollbar cannot accurately represent the /// painted. In this case, the scrollbar cannot accurately represent the
/// relative location of the visible area, or calculate the accurate delta to /// relative location of the visible area, or calculate the accurate delta to
/// apply when dragging on the thumb or tapping on the track. /// apply when dragging on the thumb or tapping on the track.
/// ///
/// Scrollbars are added to most [Scrollable] widgets by default on Desktop /// ### Interaction
/// platforms in [ScrollBehavior.buildScrollbar] as part of an app's ///
/// [ScrollConfiguration]. Scrollable widgets that do not have automatically /// Scrollbars are interactive and can use the [PrimaryScrollController] if
/// applied Scrollbars include /// a [controller] is not set. Interactive Scrollbar thumbs can be dragged along
/// the main axis of the [ScrollView] to change the [ScrollPosition]. Tapping
/// along the track exclusive of the thumb will trigger a
/// [ScrollIncrementType.page] based on the relative position to the thumb.
///
/// When using the [PrimaryScrollController], it must not be attached to more
/// than one [ScrollPosition]. [ScrollView]s that have not been provided a
/// [ScrollController] and have a [ScrollView.scrollDirection] of
/// [Axis.vertical] will automatically attach their ScrollPosition to the
/// PrimaryScrollController. Provide a unique ScrollController to each
/// [Scrollable] in this case to prevent having multiple ScrollPositions
/// attached to the PrimaryScrollController.
///
/// {@tool dartpad --template=stateful_widget_scaffold_center}
/// This sample shows an app with two scrollables in the same route. Since by
/// default, there is one [PrimaryScrollController] per route, and they both have a
/// scroll direction of [Axis.vertical], they would both try to attach to that
/// controller. The [Scrollbar] cannot support multiple positions attached to
/// the same controller, so one [ListView], and its [Scrollbar] have been
/// provided a unique [ScrollController].
///
/// Alternatively, a new PrimaryScrollController could be created above one of
/// the [ListView]s.
///
/// ```dart
/// final ScrollController _firstController = ScrollController();
///
/// @override
/// Widget build(BuildContext context) {
/// return LayoutBuilder(
/// builder: (BuildContext context, BoxConstraints constraints) {
/// return Row(
/// children: <Widget>[
/// SizedBox(
/// width: constraints.maxWidth / 2,
/// // Only one scroll position can be attached to the
/// // PrimaryScrollController if using Scrollbars. Providing a
/// // unique scroll controller to this scroll view prevents it
/// // from attaching to the PrimaryScrollController.
/// child: Scrollbar(
/// isAlwaysShown: true,
/// controller: _firstController,
/// child: ListView.builder(
/// controller: _firstController,
/// itemCount: 100,
/// itemBuilder: (BuildContext context, int index) {
/// return Padding(
/// padding: const EdgeInsets.all(8.0),
/// child: Text('Scrollable 1 : Index $index'),
/// );
/// }
/// ),
/// )
/// ),
/// SizedBox(
/// width: constraints.maxWidth / 2,
/// // This vertical scroll view has not been provided a
/// // ScrollController, so it is using the
/// // PrimaryScrollController.
/// child: Scrollbar(
/// isAlwaysShown: true,
/// child: ListView.builder(
/// itemCount: 100,
/// itemBuilder: (BuildContext context, int index) {
/// return Container(
/// height: 50,
/// color: index.isEven ? Colors.amberAccent : Colors.blueAccent,
/// child: Padding(
/// padding: const EdgeInsets.all(8.0),
/// child: Text('Scrollable 2 : Index $index'),
/// )
/// );
/// }
/// ),
/// )
/// ),
/// ],
/// );
/// }
/// );
/// }
/// ```
/// {@end-tool}
///
/// ### Automatic Scrollbars on Desktop Platforms
///
/// Scrollbars are added to most [Scrollable] widgets by default on
/// [TargetPlatformVariant.desktop] platforms. This is done through
/// [ScrollBehavior.buildScrollbar] as part of an app's
/// [ScrollConfiguration]. Scrollables that do not use the
/// [PrimaryScrollController] or have a [ScrollController] provided to them
/// will receive a unique ScrollController for use with the Scrollbar. In this
/// case, only one Scrollable can be using the PrimaryScrollController, unless
/// [interactive] is false. To prevent [Axis.vertical] Scrollables from using
/// the PrimaryScrollController, set [ScrollView.primary] to false. Scrollable
/// widgets that do not have automatically applied Scrollbars include
/// ///
/// * [EditableText] /// * [EditableText]
/// * [ListWheelScrollView] /// * [ListWheelScrollView]
...@@ -692,17 +781,66 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { ...@@ -692,17 +781,66 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// * [DropdownButton] /// * [DropdownButton]
/// {@endtemplate} /// {@endtemplate}
/// ///
// TODO(Piinks): Add code sample /// {@tool dartpad --template=stateless_widget_scaffold}
/// This sample shows a [RawScrollbar] that executes a fade animation as
/// scrolling occurs. The RawScrollbar will fade into view as the user scrolls,
/// and fade out when scrolling stops. The [GridView] uses the
/// [PrimaryScrollController] since it has an [Axis.vertical] scroll direction
/// and has not been provided a [ScrollController].
///
/// ```dart
/// Widget build(BuildContext context) {
/// return RawScrollbar(
/// child: GridView.builder(
/// itemCount: 120,
/// gridDelegate:
/// const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
/// itemBuilder: (BuildContext context, int index) {
/// return Center(
/// child: Text('item $index'),
/// );
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateful_widget_scaffold}
/// When `isAlwaysShown` is true, the scrollbar thumb will remain visible without
/// the fade animation. This requires that a [ScrollController] is provided to
/// `controller` for both the [RawScrollbar] and the [GridView].
/// Alternatively, the [PrimaryScrollController] can be used automatically so long
/// as it is attached to the singular [ScrollPosition] associated with the GridView.
///
/// ```dart
/// final ScrollController _controllerOne = ScrollController();
///
/// @override
/// Widget build(BuildContext context) {
/// return RawScrollbar(
/// controller: _controllerOne,
/// isAlwaysShown: true,
/// child: GridView.builder(
/// controller: _controllerOne,
/// itemCount: 120,
/// gridDelegate:
/// const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
/// itemBuilder: (BuildContext context, int index) {
/// return Center(
/// child: Text('item $index'),
/// );
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
/// * [ListView], which displays a linear, scrollable list of children. /// * [ListView], which displays a linear, scrollable list of children.
/// * [GridView], which displays a 2 dimensional, scrollable array of children. /// * [GridView], which displays a 2 dimensional, scrollable array of children.
// TODO(Piinks): Add support for passing a shape instead of thickness/radius.
// Will need to update painter to support as well.
// Also, expose helpful properties like main/crossAxis margins, minThumbLength,
// etc. on the RawScrollbar in follow-up changes
// part of https://github.com/flutter/flutter/issues/13253
class RawScrollbar extends StatefulWidget { class RawScrollbar extends StatefulWidget {
/// Creates a basic raw scrollbar that wraps the given [child]. /// Creates a basic raw scrollbar that wraps the given [child].
/// ///
...@@ -1001,7 +1139,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1001,7 +1139,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
_fadeoutAnimationController = AnimationController( _fadeoutAnimationController = AnimationController(
vsync: this, vsync: this,
duration: widget.fadeDuration, duration: widget.fadeDuration,
); )..addStatusListener(_validateInteractions);
_fadeoutOpacityAnimation = CurvedAnimation( _fadeoutOpacityAnimation = CurvedAnimation(
parent: _fadeoutAnimationController, parent: _fadeoutAnimationController,
curve: Curves.fastOutSlowIn, curve: Curves.fastOutSlowIn,
...@@ -1032,19 +1170,26 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1032,19 +1170,26 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
// Wait one frame and cause an empty scroll event. This allows the // Wait one frame and cause an empty scroll event. This allows the
// thumb to show immediately when isAlwaysShown is true. A scroll // thumb to show immediately when isAlwaysShown is true. A scroll
// event is required in order to paint the thumb. // event is required in order to paint the thumb.
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
scrollController!.position.didUpdateScrollPositionBy(0); scrollController!.position.didUpdateScrollPositionBy(0);
} }
// Interactive scrollbars need to be properly configured.
// If there is no scroll controller, there will not be gestures at all.
if (scrollController != null && enableGestures) {
_checkForValidScrollPosition();
}
}); });
} }
void _checkForValidScrollPosition() { void _validateInteractions(AnimationStatus status) {
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
if (status == AnimationStatus.dismissed) {
assert(_fadeoutOpacityAnimation.value == 0.0);
// We do not check for a valid scroll position if the scrollbar is not
// visible, because it cannot be interacted with.
} else if (scrollController != null && enableGestures) {
// Interactive scrollbars need to be properly configured. If it is visible
// for interaction, ensure we are set up properly.
assert(_debugCheckHasValidScrollPosition());
}
}
bool _debugCheckHasValidScrollPosition() {
final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context); final ScrollController? scrollController = widget.controller ?? PrimaryScrollController.of(context);
final bool tryPrimary = widget.controller == null; final bool tryPrimary = widget.controller == null;
final String controllerForError = tryPrimary final String controllerForError = tryPrimary
...@@ -1123,6 +1268,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1123,6 +1268,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
} }
return true; return true;
}()); }());
return true;
} }
/// This method is responsible for configuring the [scrollbarPainter] /// This method is responsible for configuring the [scrollbarPainter]
...@@ -1212,7 +1358,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1212,7 +1358,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@protected @protected
@mustCallSuper @mustCallSuper
void handleThumbPress() { void handleThumbPress() {
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
if (getScrollbarDirection() == null) { if (getScrollbarDirection() == null) {
return; return;
} }
...@@ -1225,7 +1371,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1225,7 +1371,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@protected @protected
@mustCallSuper @mustCallSuper
void handleThumbPressStart(Offset localPosition) { void handleThumbPressStart(Offset localPosition) {
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
_currentController = widget.controller ?? PrimaryScrollController.of(context); _currentController = widget.controller ?? PrimaryScrollController.of(context);
final Axis? direction = getScrollbarDirection(); final Axis? direction = getScrollbarDirection();
if (direction == null) { if (direction == null) {
...@@ -1242,7 +1388,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1242,7 +1388,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@protected @protected
@mustCallSuper @mustCallSuper
void handleThumbPressUpdate(Offset localPosition) { void handleThumbPressUpdate(Offset localPosition) {
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
final Axis? direction = getScrollbarDirection(); final Axis? direction = getScrollbarDirection();
if (direction == null) { if (direction == null) {
return; return;
...@@ -1255,7 +1401,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1255,7 +1401,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
@protected @protected
@mustCallSuper @mustCallSuper
void handleThumbPressEnd(Offset localPosition, Velocity velocity) { void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
final Axis? direction = getScrollbarDirection(); final Axis? direction = getScrollbarDirection();
if (direction == null) { if (direction == null) {
return; return;
...@@ -1267,7 +1413,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv ...@@ -1267,7 +1413,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
void _handleTrackTapDown(TapDownDetails details) { void _handleTrackTapDown(TapDownDetails details) {
// The Scrollbar should page towards the position of the tap on the track. // The Scrollbar should page towards the position of the tap on the track.
_checkForValidScrollPosition(); assert(_debugCheckHasValidScrollPosition());
_currentController = widget.controller ?? PrimaryScrollController.of(context); _currentController = widget.controller ?? PrimaryScrollController.of(context);
double scrollIncrement; double scrollIncrement;
......
...@@ -360,7 +360,7 @@ void main() { ...@@ -360,7 +360,7 @@ void main() {
} }
await tester.pumpWidget(viewWithScroll()); await tester.pumpWidget(viewWithScroll());
final dynamic exception = tester.takeException(); final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError); expect(exception, isAssertionError);
}, },
); );
...@@ -390,7 +390,7 @@ void main() { ...@@ -390,7 +390,7 @@ void main() {
} }
await tester.pumpWidget(viewWithScroll()); await tester.pumpWidget(viewWithScroll());
final dynamic exception = tester.takeException(); final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError); expect(exception, isAssertionError);
}, },
); );
...@@ -961,10 +961,16 @@ void main() { ...@@ -961,10 +961,16 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final dynamic exception = tester.takeException(); 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, isAssertionError);
expect( expect(
(exception as AssertionError).message, exception.message,
contains("The Scrollbar's ScrollController has no ScrollPosition attached."), contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
); );
}); });
......
...@@ -128,7 +128,7 @@ void main() { ...@@ -128,7 +128,7 @@ void main() {
of: find.byType(Scrollbar), of: find.byType(Scrollbar),
matching: find.byType(CustomPaint), matching: find.byType(CustomPaint),
).first); ).first);
final dynamic scrollPainter = custom.foregroundPainter; final ScrollbarPainter? scrollPainter = custom.foregroundPainter as ScrollbarPainter?;
// Dragging makes the scrollbar first appear. // Dragging makes the scrollbar first appear.
await tester.drag(find.text('0'), const Offset(0.0, -10.0)); 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));
...@@ -141,7 +141,7 @@ void main() { ...@@ -141,7 +141,7 @@ void main() {
viewportDimension: 100.0, viewportDimension: 100.0,
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
); );
scrollPainter.update(metrics, AxisDirection.down); scrollPainter!.update(metrics, AxisDirection.down);
final TestCanvas canvas = TestCanvas(); final TestCanvas canvas = TestCanvas();
scrollPainter.paint(canvas, const Size(10.0, 100.0)); scrollPainter.paint(canvas, const Size(10.0, 100.0));
...@@ -171,7 +171,7 @@ void main() { ...@@ -171,7 +171,7 @@ void main() {
} }
await tester.pumpWidget(viewWithScroll()); await tester.pumpWidget(viewWithScroll());
final dynamic exception = tester.takeException(); final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError); expect(exception, isAssertionError);
}, },
); );
...@@ -199,7 +199,7 @@ void main() { ...@@ -199,7 +199,7 @@ void main() {
} }
await tester.pumpWidget(viewWithScroll()); await tester.pumpWidget(viewWithScroll());
final dynamic exception = tester.takeException(); final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError); expect(exception, isAssertionError);
}, },
); );
......
...@@ -381,13 +381,13 @@ void main() { ...@@ -381,13 +381,13 @@ void main() {
axisDirection: AxisDirection.down, axisDirection: AxisDirection.down,
); );
final ScrollbarPainter p = _buildPainter( final ScrollbarPainter painter = _buildPainter(
padding: padding, padding: padding,
scrollMetrics: metrics, scrollMetrics: metrics,
); );
testWidgets('down', (WidgetTester tester) async { testWidgets('down', (WidgetTester tester) async {
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.height, viewportDimension: size.height,
pixels: double.negativeInfinity, pixels: double.negativeInfinity,
...@@ -396,13 +396,13 @@ void main() { ...@@ -396,13 +396,13 @@ void main() {
); );
// Top overscroll. // Top overscroll.
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect0 = captureRect(); final Rect rect0 = captureRect();
expect(rect0.top, padding.top); expect(rect0.top, padding.top);
expect(size.width - rect0.right, padding.right); expect(size.width - rect0.right, padding.right);
// Bottom overscroll. // Bottom overscroll.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.height, viewportDimension: size.height,
pixels: double.infinity, pixels: double.infinity,
...@@ -410,14 +410,14 @@ void main() { ...@@ -410,14 +410,14 @@ void main() {
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect1 = captureRect(); final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom); expect(size.height - rect1.bottom, padding.bottom);
expect(size.width - rect1.right, padding.right); expect(size.width - rect1.right, padding.right);
}); });
testWidgets('up', (WidgetTester tester) async { testWidgets('up', (WidgetTester tester) async {
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.height, viewportDimension: size.height,
pixels: double.infinity, pixels: double.infinity,
...@@ -427,13 +427,13 @@ void main() { ...@@ -427,13 +427,13 @@ void main() {
); );
// Top overscroll. // Top overscroll.
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect0 = captureRect(); final Rect rect0 = captureRect();
expect(rect0.top, padding.top); expect(rect0.top, padding.top);
expect(size.width - rect0.right, padding.right); expect(size.width - rect0.right, padding.right);
// Bottom overscroll. // Bottom overscroll.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.height, viewportDimension: size.height,
pixels: double.negativeInfinity, pixels: double.negativeInfinity,
...@@ -442,14 +442,14 @@ void main() { ...@@ -442,14 +442,14 @@ void main() {
AxisDirection.up, AxisDirection.up,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect1 = captureRect(); final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom); expect(size.height - rect1.bottom, padding.bottom);
expect(size.width - rect1.right, padding.right); expect(size.width - rect1.right, padding.right);
}); });
testWidgets('left', (WidgetTester tester) async { testWidgets('left', (WidgetTester tester) async {
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.width, viewportDimension: size.width,
pixels: double.negativeInfinity, pixels: double.negativeInfinity,
...@@ -459,13 +459,13 @@ void main() { ...@@ -459,13 +459,13 @@ void main() {
); );
// Right overscroll. // Right overscroll.
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect0 = captureRect(); final Rect rect0 = captureRect();
expect(size.height - rect0.bottom, padding.bottom); expect(size.height - rect0.bottom, padding.bottom);
expect(size.width - rect0.right, padding.right); expect(size.width - rect0.right, padding.right);
// Left overscroll. // Left overscroll.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.width, viewportDimension: size.width,
pixels: double.infinity, pixels: double.infinity,
...@@ -474,14 +474,14 @@ void main() { ...@@ -474,14 +474,14 @@ void main() {
AxisDirection.left, AxisDirection.left,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect1 = captureRect(); final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom); expect(size.height - rect1.bottom, padding.bottom);
expect(rect1.left, padding.left); expect(rect1.left, padding.left);
}); });
testWidgets('right', (WidgetTester tester) async { testWidgets('right', (WidgetTester tester) async {
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.width, viewportDimension: size.width,
pixels: double.infinity, pixels: double.infinity,
...@@ -491,13 +491,13 @@ void main() { ...@@ -491,13 +491,13 @@ void main() {
); );
// Right overscroll. // Right overscroll.
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect0 = captureRect(); final Rect rect0 = captureRect();
expect(size.height - rect0.bottom, padding.bottom); expect(size.height - rect0.bottom, padding.bottom);
expect(size.width - rect0.right, padding.right); expect(size.width - rect0.right, padding.right);
// Left overscroll. // Left overscroll.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
viewportDimension: size.width, viewportDimension: size.width,
pixels: double.negativeInfinity, pixels: double.negativeInfinity,
...@@ -506,7 +506,7 @@ void main() { ...@@ -506,7 +506,7 @@ void main() {
AxisDirection.right, AxisDirection.right,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final Rect rect1 = captureRect(); final Rect rect1 = captureRect();
expect(size.height - rect1.bottom, padding.bottom); expect(size.height - rect1.bottom, padding.bottom);
expect(rect1.left, padding.left); expect(rect1.left, padding.left);
...@@ -525,7 +525,7 @@ void main() { ...@@ -525,7 +525,7 @@ void main() {
); );
const double minOverscrollLength = 8.0; const double minOverscrollLength = 8.0;
final ScrollbarPainter p = _buildPainter( final ScrollbarPainter painter = _buildPainter(
padding: padding, padding: padding,
scrollMetrics: metrics, scrollMetrics: metrics,
minLength: 36.0, minLength: 36.0,
...@@ -533,54 +533,54 @@ void main() { ...@@ -533,54 +533,54 @@ void main() {
); );
// No overscroll gives a full sized thumb. // No overscroll gives a full sized thumb.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
pixels: 0.0, pixels: 0.0,
), ),
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
final double fullThumbExtent = captureRect().height; final double fullThumbExtent = captureRect().height;
expect(fullThumbExtent, greaterThan(_kMinThumbExtent)); expect(fullThumbExtent, greaterThan(_kMinThumbExtent));
// Scrolling to the middle also gives a full sized thumb. // Scrolling to the middle also gives a full sized thumb.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
pixels: scrollExtent / 2, pixels: scrollExtent / 2,
), ),
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6)); expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6));
// Scrolling just to the very end also gives a full sized thumb. // Scrolling just to the very end also gives a full sized thumb.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
pixels: scrollExtent, pixels: scrollExtent,
), ),
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6)); expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 1e-6));
// Scrolling just past the end shrinks the thumb slightly. // Scrolling just past the end shrinks the thumb slightly.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
pixels: scrollExtent * 1.001, pixels: scrollExtent * 1.001,
), ),
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 2.0)); expect(captureRect().height, moreOrLessEquals(fullThumbExtent, epsilon: 2.0));
// Scrolling way past the end shrinks the thumb to minimum. // Scrolling way past the end shrinks the thumb to minimum.
p.update( painter.update(
metrics.copyWith( metrics.copyWith(
pixels: double.infinity, pixels: double.infinity,
), ),
AxisDirection.down, AxisDirection.down,
); );
p.paint(testCanvas, size); painter.paint(testCanvas, size);
expect(captureRect().height, minOverscrollLength); expect(captureRect().height, minOverscrollLength);
}); });
...@@ -1163,10 +1163,10 @@ void main() { ...@@ -1163,10 +1163,10 @@ void main() {
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final dynamic exception = tester.takeException(); final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError); expect(exception, isAssertionError);
expect( expect(
(exception as AssertionError).message, exception.message,
contains("The Scrollbar's ScrollController has no ScrollPosition attached."), contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
); );
}); });
...@@ -1198,10 +1198,16 @@ void main() { ...@@ -1198,10 +1198,16 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final dynamic exception = tester.takeException(); 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, isAssertionError);
expect( expect(
(exception as AssertionError).message, exception.message,
contains("The Scrollbar's ScrollController has no ScrollPosition attached."), contains("The Scrollbar's ScrollController has no ScrollPosition attached."),
); );
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment