mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
1831 lines
58 KiB
Dart
1831 lines
58 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// 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();
|
|
|
|
if (!kIsWeb) {
|
|
// Scrolling while holding the drag on the scrollbar and still hovered over
|
|
// the scrollbar should not have changed the scroll offset.
|
|
expect(pointer.location, const Offset(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),
|
|
),
|
|
);
|
|
} else {
|
|
expect(pointer.location, const Offset(798.0, 15.0));
|
|
expect(scrollController.offset, previousOffset + 20.0);
|
|
}
|
|
|
|
// Drag is still being held, move pointer to be hovering over another area
|
|
// of the scrollable (not over the scrollbar) and execute another pointer scroll
|
|
pointer.hover(tester.getCenter(find.byType(SingleChildScrollView)));
|
|
await tester.sendEventToBinding(pointer.scroll(const Offset(0.0, -90.0)));
|
|
await tester.pumpAndSettle();
|
|
// Scrolling while holding the drag on the scrollbar changed the offset
|
|
expect(pointer.location, const Offset(400.0, 300.0));
|
|
expect(scrollController.offset, 0.0);
|
|
expect(
|
|
find.byType(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,
|
|
),
|
|
);
|
|
});
|
|
}
|