blob: 00a2b0aae983628c3e37c7e3aef11637c9bb6d4b [file] [log] [blame] [edit]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
// The const represents the starting position of the scrollbar thumb for
// the below tests. The thumb is 90 pixels long, and 8 pixels wide, with a 2
// pixel margin to the right edge of the viewport.
const Rect _kMaterialDesignInitialThumbRect = Rect.fromLTRB(790.0, 0.0, 798.0, 90.0);
const Radius _kDefaultThumbRadius = Radius.circular(8.0);
const Color _kDefaultIdleThumbColor = Color(0x1a000000);
const Color _kDefaultDragThumbColor = Color(0x99000000);
void main() {
test('ScrollbarThemeData copyWith, ==, hashCode basics', () {
expect(const ScrollbarThemeData(), const ScrollbarThemeData().copyWith());
expect(const ScrollbarThemeData().hashCode, const ScrollbarThemeData().copyWith().hashCode);
});
testWidgets('Passing no ScrollbarTheme returns defaults', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
// Drag scrollbar behavior
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..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
// Drag color
color: _kDefaultDragThumbColor,
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
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
..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: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 10.0, 798.0, 100.0),
_kDefaultThumbRadius,
),
// Hover color
color: const Color(0x80000000),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Scrollbar uses values from ScrollbarTheme', (WidgetTester tester) async {
final ScrollbarThemeData scrollbarTheme = _scrollbarTheme();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
scrollbarTheme: scrollbarTheme,
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(785.0, 10.0, 795.0, 97.0),
const Radius.circular(6.0),
),
color: const Color(0xff4caf50),
),
);
// Drag scrollbar behavior
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..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(785.0, 10.0, 795.0, 97.0),
const Radius.circular(6.0),
),
// Drag color
color: const Color(0xfff44336),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture gesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(const Offset(794.0, 15.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(770.0, 0.0, 800.0, 600.0),
color: const Color(0xff000000),
)
..line(
p1: const Offset(770.0, 00.0),
p2: const Offset(770.0, 600.0),
strokeWidth: 1.0,
color: const Color(0xffffeb3b),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(775.0, 20.0, 795.0, 107.0),
const Radius.circular(6.0),
),
// Hover color
color: const Color(0xff2196f3),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets(
'Scrollbar uses values from ScrollbarTheme if exists instead of values from Theme',
(WidgetTester tester) async {
final ScrollbarThemeData scrollbarTheme = _scrollbarTheme();
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
scrollbarTheme: scrollbarTheme,
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: ScrollbarTheme(
data: _scrollbarTheme().copyWith(
thumbColor: MaterialStateProperty.all(const Color(0xFF000000)),
),
child: Scrollbar(
thumbVisibility: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
),
);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints
..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(785.0, 10.0, 795.0, 97.0),
const Radius.circular(6.0),
),
color: const Color(0xFF000000),
),
);
},
);
testWidgets('ScrollbarTheme can disable gestures', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(interactive: false)),
home: Scrollbar(
isAlwaysShown: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
// Try to drag scrollbar.
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Expect no change
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgets('Scrollbar.interactive takes priority over ScrollbarTheme', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(scrollbarTheme: const ScrollbarThemeData(interactive: false)),
home: Scrollbar(
interactive: true,
isAlwaysShown: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
// Drag scrollbar.
const double scrollAmount = 10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Gestures handled by Scrollbar.
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 10.0, 798.0, 100.0),
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.fuchsia }));
testWidgets('Scrollbar widget properties take priority over theme', (WidgetTester tester) async {
const double thickness = 4.0;
const double hoverThickness = 4.0;
const bool showTrackOnHover = true;
const Radius radius = Radius.circular(3.0);
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
colorScheme: const ColorScheme.light(),
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
thickness: thickness,
hoverThickness: hoverThickness,
thumbVisibility: true,
showTrackOnHover: showTrackOnHover,
radius: radius,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 0.0, 798.0, 90.0),
const Radius.circular(3.0),
),
color: _kDefaultIdleThumbColor,
),
);
// Drag scrollbar behavior
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..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(794.0, 0.0, 798.0, 90.0),
const Radius.circular(3.0),
),
// Drag color
color: _kDefaultDragThumbColor,
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
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
..rect(
rect: const Rect.fromLTRB(792.0, 0.0, 800.0, 600.0),
color: const Color(0x08000000),
)
..line(
p1: const Offset(792.0, 0.0),
p2: const Offset(792.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(794.0, 10.0, 798.0, 100.0),
const Radius.circular(3.0),
),
// Hover color
color: const Color(0x80000000),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('ThemeData colorScheme is used when no ScrollbarTheme is set', (WidgetTester tester) async {
Widget buildFrame(ThemeData appTheme) {
final ScrollController scrollController = ScrollController();
return MaterialApp(
theme: appTheme,
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
);
}
// Scrollbar defaults for light themes:
// - coloring based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData(
colorScheme: const ColorScheme.light(),
)));
await tester.pumpAndSettle();
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
color: _kDefaultIdleThumbColor,
),
);
// Drag scrollbar behavior
const double scrollAmount = 10.0;
TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
_kMaterialDesignInitialThumbRect,
_kDefaultThumbRadius,
),
// Drag color
color: _kDefaultDragThumbColor,
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
final TestGesture hoverGesture = await tester.createGesture(kind: ui.PointerDeviceKind.mouse);
await hoverGesture.addPointer();
addTearDown(hoverGesture.removePointer);
await hoverGesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
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: const Color(0x1a000000),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 10.0, 798.0, 100.0),
_kDefaultThumbRadius,
),
// Hover color
color: const Color(0x80000000),
),
);
await hoverGesture.moveTo(Offset.zero);
// Scrollbar defaults for dark themes:
// - coloring slightly different based on ColorScheme.onSurface
await tester.pumpWidget(buildFrame(ThemeData(
colorScheme: const ColorScheme.dark(),
)));
await tester.pumpAndSettle(); // Theme change animation
// Idle scrollbar behavior
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 10.0, 798.0, 100.0),
_kDefaultThumbRadius,
),
color: const Color(0x4dffffff),
),
);
// Drag scrollbar behavior
dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 45.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints..rrect(
rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(790.0, 10.0, 798.0, 100.0),
_kDefaultThumbRadius,
),
// Drag color
color: const Color(0xbfffffff),
),
);
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// Hover scrollbar behavior
await hoverGesture.moveTo(const Offset(794.0, 5.0));
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(
rect: const Rect.fromLTRB(784.0, 0.0, 800.0, 600.0),
color: const Color(0x0dffffff),
)
..line(
p1: const Offset(784.0, 0.0),
p2: const Offset(784.0, 600.0),
strokeWidth: 1.0,
color: const Color(0x40ffffff),
)
..rrect(
rrect: RRect.fromRectAndRadius(
// Scrollbar thumb is larger
const Rect.fromLTRB(786.0, 20.0, 798.0, 110.0),
_kDefaultThumbRadius,
),
// Hover color
color: const Color(0xa6ffffff),
),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('ScrollbarThemeData.trackVisibility test', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
bool? getTrackVisibility(Set<MaterialState> states) {
return true;
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeData().copyWith(
scrollbarTheme: _scrollbarTheme(
trackVisibility: MaterialStateProperty.resolveWith(getTrackVisibility),
),
),
home: ScrollConfiguration(
behavior: const NoScrollbarBehavior(),
child: Scrollbar(
isAlwaysShown: true,
showTrackOnHover: true,
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
child: const SizedBox(width: 4000.0, height: 4000.0),
),
),
),
),
);
await tester.pumpAndSettle();
expect(
find.byType(Scrollbar),
paints
..rect(color: const Color(0x08000000))
..line(
strokeWidth: 1.0,
color: const Color(0x1a000000),
)
..rrect(color: const Color(0xff4caf50)),
);
}, variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.linux,
TargetPlatform.macOS,
TargetPlatform.windows,
TargetPlatform.fuchsia,
}),
);
testWidgets('Default ScrollbarTheme debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const ScrollbarThemeData().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[]);
});
testWidgets('ScrollbarTheme implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
ScrollbarThemeData(
thickness: MaterialStateProperty.resolveWith(_getThickness),
showTrackOnHover: true,
thumbVisibility: MaterialStateProperty.resolveWith(_getThumbVisibility),
radius: const Radius.circular(3.0),
thumbColor: MaterialStateProperty.resolveWith(_getThumbColor),
trackColor: MaterialStateProperty.resolveWith(_getTrackColor),
trackBorderColor: MaterialStateProperty.resolveWith(_getTrackBorderColor),
crossAxisMargin: 3.0,
mainAxisMargin: 6.0,
minThumbLength: 120.0,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>[
"thumbVisibility: Instance of '_MaterialStatePropertyWith<bool?>'",
"thickness: Instance of '_MaterialStatePropertyWith<double?>'",
'showTrackOnHover: true',
'radius: Radius.circular(3.0)',
"thumbColor: Instance of '_MaterialStatePropertyWith<Color?>'",
"trackColor: Instance of '_MaterialStatePropertyWith<Color?>'",
"trackBorderColor: Instance of '_MaterialStatePropertyWith<Color?>'",
'crossAxisMargin: 3.0',
'mainAxisMargin: 6.0',
'minThumbLength: 120.0',
]);
// On the web, Dart doubles and ints are backed by the same kind of object because
// JavaScript does not support integers. So, the Dart double "4.0" is identical
// to "4", which results in the web evaluating to the value "4" regardless of which
// one is used. This results in a difference for doubles in debugFillProperties between
// the web and the rest of Flutter's target platforms.
}, skip: kIsWeb); // [intended]
}
class NoScrollbarBehavior extends ScrollBehavior {
const NoScrollbarBehavior();
@override
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) => child;
}
ScrollbarThemeData _scrollbarTheme({
MaterialStateProperty<double?>? thickness,
MaterialStateProperty<bool?>? trackVisibility,
bool showTrackOnHover = true,
MaterialStateProperty<bool?>? thumbVisibility,
Radius radius = const Radius.circular(6.0),
MaterialStateProperty<Color?>? thumbColor,
MaterialStateProperty<Color?>? trackColor,
MaterialStateProperty<Color?>? trackBorderColor,
double crossAxisMargin = 5.0,
double mainAxisMargin = 10.0,
double minThumbLength = 50.0,
}) {
return ScrollbarThemeData(
thickness: thickness ?? MaterialStateProperty.resolveWith(_getThickness),
trackVisibility: trackVisibility,
showTrackOnHover: showTrackOnHover,
thumbVisibility: thumbVisibility,
radius: radius,
thumbColor: thumbColor ?? MaterialStateProperty.resolveWith(_getThumbColor),
trackColor: trackColor ?? MaterialStateProperty.resolveWith(_getTrackColor),
trackBorderColor: trackBorderColor ?? MaterialStateProperty.resolveWith(_getTrackBorderColor),
crossAxisMargin: crossAxisMargin,
mainAxisMargin: mainAxisMargin,
minThumbLength: minThumbLength,
);
}
double? _getThickness(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return 20.0;
}
return 10.0;
}
bool? _getThumbVisibility(Set<MaterialState> states) => true;
Color? _getThumbColor(Set<MaterialState> states) {
if (states.contains(MaterialState.dragged)) {
return Colors.red;
}
if (states.contains(MaterialState.hovered)) {
return Colors.blue;
}
return Colors.green;
}
Color? _getTrackColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.black;
}
return null;
}
Color? _getTrackBorderColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.yellow;
}
return null;
}