flutter/packages/flutter/test/material/slider_test.dart
Ian Hickson 449f4a6673
License update (#45373)
* Update project.pbxproj files to say Flutter rather than Chromium

Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright.

* Update the copyright notice checker to require a standard notice on all files

* Update copyrights on Dart files. (This was a mechanical commit.)

* Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine.

Some were already marked "The Flutter Authors", not clear why. Their
dates have been normalized. Some were missing the blank line after the
license. Some were randomly different in trivial ways for no apparent
reason (e.g. missing the trailing period).

* Clean up the copyrights in non-Dart files. (Manual edits.)

Also, make sure templates don't have copyrights.

* Fix some more ORGANIZATIONNAMEs
2019-11-27 15:04:02 -08:00

1584 lines
54 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.
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';
// A thumb shape that also logs its repaint center.
class LoggingThumbShape extends SliderComponentShape {
LoggingThumbShape(this.log);
final List<Offset> log;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return const Size(10.0, 10.0);
}
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double> activationAnimation,
Animation<double> enableAnimation,
bool isEnabled,
bool isDiscrete,
bool onActiveTrack,
TextPainter labelPainter,
RenderBox parentBox,
SliderThemeData sliderTheme,
TextDirection textDirection,
double value,
}) {
log.add(thumbCenter);
final Paint thumbPaint = Paint()..color = Colors.red;
context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint);
}
}
class TallSliderTickMarkShape extends SliderTickMarkShape {
@override
Size getPreferredSize({SliderThemeData sliderTheme, bool isEnabled}) {
return const Size(10.0, 200.0);
}
@override
void paint(
PaintingContext context,
Offset offset, {
Offset thumbCenter,
RenderBox parentBox,
SliderThemeData sliderTheme,
Animation<double> enableAnimation,
bool isEnabled,
TextDirection textDirection,
}) {
final Paint paint = Paint()..color = Colors.red;
context.canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, 10.0, 20.0), paint);
}
}
void main() {
testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
double startValue;
double endValue;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
onChangeStart: (double value) {
startValue = value;
},
onChangeEnd: (double value) {
endValue = value;
},
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
expect(startValue, equals(0.0));
expect(endValue, equals(0.5));
startValue = null;
endValue = null;
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
await tester.tapAt(target);
expect(value, closeTo(0.25, 0.05));
expect(startValue, equals(0.5));
expect(endValue, closeTo(0.25, 0.05));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider can move when tapped (RTL)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
final Offset target = topLeft + (bottomRight - topLeft) / 4.0;
await tester.tapAt(target);
expect(value, closeTo(0.75, 0.05));
await tester.pump(); // No animation should start.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
double startValue;
double endValue;
int updates = 0;
int startValueUpdates = 0;
int endValueUpdates = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
updates++;
value = newValue;
});
},
onChangeStart: (double value) {
startValueUpdates++;
startValue = value;
},
onChangeEnd: (double value) {
endValueUpdates++;
endValue = value;
},
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
expect(startValue, equals(0.0));
expect(endValue, equals(0.5));
await tester.pump();
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump();
expect(updates, equals(1));
expect(startValueUpdates, equals(2));
expect(endValueUpdates, equals(2));
});
testWidgets('Value indicator shows for a bit after being tapped', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump(const Duration(milliseconds: 100));
// Starts with the position animation and value indicator
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
await tester.pump(const Duration(milliseconds: 100));
// Value indicator is longer than position.
expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
await tester.pump(const Duration(milliseconds: 100));
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
await tester.pump(const Duration(milliseconds: 100));
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
await tester.pump(const Duration(milliseconds: 100));
// Shown for long enough, value indicator is animated closed.
expect(SchedulerBinding.instance.transientCallbackCount, equals(1));
await tester.pump(const Duration(milliseconds: 101));
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb);
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
);
},
),
),
);
final List<Offset> expectedLog = <Offset>[
const Offset(24.0, 300.0),
const Offset(24.0, 300.0),
const Offset(400.0, 300.0),
];
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(value, equals(0.5));
expect(log.length, 3);
expect(log, orderedEquals(expectedLog));
await gesture.moveBy(const Offset(-500.0, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(value, equals(0.0));
expect(log.length, 5);
expect(log.last.dx, closeTo(386.6, 0.1));
// With no more gesture or value changes, the thumb position should still
// be redrawn in the animated position.
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(value, equals(0.0));
expect(log.length, 7);
expect(log.last.dx, closeTo(344.5, 0.1));
// Final position.
await tester.pump(const Duration(milliseconds: 80));
expectedLog.add(const Offset(24.0, 300.0));
expect(value, equals(0.0));
expect(log.length, 8);
expect(log.last.dx, closeTo(24.0, 0.1));
await gesture.up();
});
testWidgets("Slider doesn't send duplicate change events if tapped on the same value", (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
int updates = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
updates++;
value = newValue;
});
},
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump();
await tester.tap(find.byKey(sliderKey));
expect(value, equals(0.5));
await tester.pump();
expect(updates, equals(1));
});
testWidgets('discrete Slider repaints when dragged', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
final List<Offset> log = <Offset>[];
final LoggingThumbShape loggingThumb = LoggingThumbShape(log);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(thumbShape: loggingThumb);
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SliderTheme(
data: sliderTheme,
child: Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
);
},
),
),
);
final List<Offset> expectedLog = <Offset>[
const Offset(24.0, 300.0),
const Offset(24.0, 300.0),
const Offset(400.0, 300.0),
];
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(value, equals(0.5));
expect(log.length, 3);
expect(log, orderedEquals(expectedLog));
await gesture.moveBy(const Offset(-500.0, 0.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(value, equals(0.0));
expect(log.length, 5);
expect(log.last.dx, closeTo(386.6, 0.1));
// With no more gesture or value changes, the thumb position should still
// be redrawn in the animated position.
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(value, equals(0.0));
expect(log.length, 7);
expect(log.last.dx, closeTo(344.5, 0.1));
// Final position.
await tester.pump(const Duration(milliseconds: 80));
expectedLog.add(const Offset(24.0, 300.0));
expect(value, equals(0.0));
expect(log.length, 8);
expect(log.last.dx, closeTo(24.0, 0.1));
await gesture.up();
});
testWidgets('Slider take on discrete values', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: SizedBox(
width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth
child: Slider(
key: sliderKey,
min: 0.0,
max: 100.0,
divisions: 10,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
);
},
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey));
expect(value, equals(50.0));
await tester.drag(find.byKey(sliderKey), const Offset(5.0, 0.0));
expect(value, equals(50.0));
await tester.drag(find.byKey(sliderKey), const Offset(40.0, 0.0));
expect(value, equals(80.0));
await tester.pump(); // Starts animation.
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));
// Animation complete.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider can be given zero values', (WidgetTester tester) async {
final List<double> log = <double>[];
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Slider(
value: 0.0,
min: 0.0,
max: 1.0,
onChanged: (double newValue) {
log.add(newValue);
},
),
),
),
));
await tester.tap(find.byType(Slider));
expect(log, <double>[0.5]);
log.clear();
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Slider(
value: 0.0,
min: 0.0,
max: 0.0,
onChanged: (double newValue) {
log.add(newValue);
},
),
),
),
));
await tester.tap(find.byType(Slider));
expect(log, <double>[]);
log.clear();
});
testWidgets('Slider uses the right theme colors for the right components', (WidgetTester tester) async {
const Color customColor1 = Color(0xcafefeed);
const Color customColor2 = Color(0xdeadbeef);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
disabledThumbColor: Color(0xff000001),
disabledActiveTickMarkColor: Color(0xff000002),
disabledActiveTrackColor: Color(0xff000003),
disabledInactiveTickMarkColor: Color(0xff000004),
disabledInactiveTrackColor: Color(0xff000005),
activeTrackColor: Color(0xff000006),
activeTickMarkColor: Color(0xff000007),
inactiveTrackColor: Color(0xff000008),
inactiveTickMarkColor: Color(0xff000009),
overlayColor: Color(0xff000010),
thumbColor: Color(0xff000011),
valueIndicatorColor: Color(0xff000012),
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
double value = 0.45;
Widget buildApp({
Color activeColor,
Color inactiveColor,
int divisions,
bool enabled = true,
}) {
final ValueChanged<double> onChanged = !enabled
? null
: (double d) {
value = d;
};
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: theme,
child: Slider(
value: value,
label: '$value',
divisions: divisions,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp());
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// Check default theme for enabled widget.
expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
// Test setting only the activeColor.
await tester.pumpWidget(buildApp(activeColor: customColor1));
expect(sliderBox, paints..rect(color: customColor1)..rect(color: sliderTheme.inactiveTrackColor));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
// Test setting only the inactiveColor.
await tester.pumpWidget(buildApp(inactiveColor: customColor1));
expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: customColor1));
expect(sliderBox, paints..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
// Test setting both activeColor and inactiveColor.
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2));
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
expect(sliderBox, paints..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
// Test colors for discrete slider.
await tester.pumpWidget(buildApp(divisions: 3));
expect(sliderBox, paints..rect(color: sliderTheme.activeTrackColor)..rect(color: sliderTheme.inactiveTrackColor));
expect(
sliderBox,
paints
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.thumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
// Test colors for discrete slider with inactiveColor and activeColor set.
await tester.pumpWidget(buildApp(
activeColor: customColor1,
inactiveColor: customColor2,
divisions: 3,
));
expect(sliderBox, paints..rect(color: customColor1)..rect(color: customColor2));
expect(
sliderBox,
paints
..circle(color: customColor2)
..circle(color: customColor2)
..circle(color: customColor1)
..circle(color: customColor1)
..circle(color: customColor1));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor)));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor)));
// Test default theme for disabled widget.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveTrackColor)
..rect(color: sliderTheme.disabledInactiveTrackColor));
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor)));
// Test setting the activeColor and inactiveColor for disabled widget.
await tester.pumpWidget(buildApp(activeColor: customColor1, inactiveColor: customColor2, enabled: false));
expect(
sliderBox,
paints
..rect(color: sliderTheme.disabledActiveTrackColor)
..rect(color: sliderTheme.disabledInactiveTrackColor));
expect(sliderBox, paints..circle(color: sliderTheme.disabledThumbColor));
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor)));
// Test that the default value indicator has the right colors.
await tester.pumpWidget(buildApp(divisions: 3));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
paints
..rect(color: sliderTheme.activeTrackColor)
..rect(color: sliderTheme.inactiveTrackColor)
..circle(color: sliderTheme.overlayColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..path(color: sliderTheme.valueIndicatorColor)
..circle(color: sliderTheme.thumbColor),
);
await gesture.up();
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
// Testing the custom colors are used for the indicator.
await tester.pumpWidget(buildApp(
divisions: 3,
activeColor: customColor1,
inactiveColor: customColor2,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(value, equals(2.0 / 3.0));
expect(
sliderBox,
paints
..rect(color: customColor1) // active track
..rect(color: customColor2) // inactive track
..circle(color: customColor1.withOpacity(0.12)) // overlay
..circle(color: customColor2) // 1st tick mark
..circle(color: customColor2) // 2nd tick mark
..circle(color: customColor2) // 3rd tick mark
..circle(color: customColor1) // 4th tick mark
..path(color: customColor1) // indicator
..circle(color: customColor1), // thumb
);
await gesture.up();
});
testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: ListView(
children: <Widget>[
Slider(
value: value,
onChanged: (double newValue) {
value = newValue;
},
),
Container(
height: 2000.0,
),
],
),
),
),
));
await tester.tap(find.byType(Slider));
expect(value, equals(0.5));
});
testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
value: value,
onChanged: (double newValue) {
value = newValue;
},
),
),
),
),
));
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
expect(value, equals(0.5));
await gesture.moveBy(const Offset(1.0, 0.0));
expect(value, greaterThan(0.5));
await gesture.up();
});
testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async {
double value = 0.0;
await tester.pumpWidget(Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
value: value,
onChanged: (double newValue) {
value = newValue;
},
),
),
),
),
));
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
expect(value, equals(0.5));
await gesture.moveBy(const Offset(1.0, 0.0));
expect(value, lessThan(0.5));
await gesture.up();
});
testWidgets('Slider sizing', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: const Material(
child: Center(
child: Slider(
value: 0.5,
onChanged: null,
),
),
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(800.0, 600.0));
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: const Material(
child: Center(
child: IntrinsicWidth(
child: Slider(
value: 0.5,
onChanged: null,
),
),
),
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 600.0));
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: const Material(
child: Center(
child: OverflowBox(
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Slider(
value: 0.5,
onChanged: null,
),
),
),
),
),
));
expect(tester.renderObject<RenderBox>(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0));
});
testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
Widget buildSlider({
double textScaleFactor,
bool isDiscrete = true,
ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Material(
child: Theme(
data: Theme.of(context).copyWith(
sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show),
),
child: Center(
child: OverflowBox(
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Slider(
key: sliderKey,
min: 0.0,
max: 100.0,
divisions: isDiscrete ? 10 : null,
label: '${value.round()}',
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
),
);
},
),
);
}
await tester.pumpWidget(buildSlider(textScaleFactor: 1.0));
Offset center = tester.getCenter(find.byType(Slider));
TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pumpAndSettle();
await tester.pumpWidget(buildSlider(textScaleFactor: 2.0));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pumpAndSettle();
// Check continuous
await tester.pumpWidget(buildSlider(
textScaleFactor: 1.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 1.0, y: 1.0));
await gesture.up();
await tester.pumpAndSettle();
await tester.pumpWidget(buildSlider(
textScaleFactor: 2.0,
isDiscrete: false,
show: ShowValueIndicator.onlyForContinuous,
));
center = tester.getCenter(find.byType(Slider));
gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
expect(tester.renderObject(find.byType(Slider)), paints..scale(x: 2.0, y: 2.0));
await gesture.up();
await tester.pumpAndSettle();
}, skip: isBrowser);
testWidgets('Tick marks are skipped when they are too dense', (WidgetTester tester) async {
Widget buildSlider({
int divisions,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
min: 0.0,
max: 100.0,
divisions: divisions,
value: 0.25,
onChanged: (double newValue) { },
),
),
),
),
);
}
// Pump a slider with a reasonable amount of divisions to verify that the
// tick marks are drawn when the number of tick marks is not too dense.
await tester.pumpWidget(
buildSlider(
divisions: 4,
),
);
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
// 5 tick marks and a thumb.
expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 6));
// 200 divisions will produce a tick interval off less than 6,
// which would be too dense to draw.
await tester.pumpWidget(
buildSlider(
divisions: 200,
),
);
// No tick marks are drawn because they are too dense, but the thumb is
// still drawn.
expect(sliderBox, paintsExactlyCountTimes(#drawCircle, 1));
});
testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async {
final Key sliderKey = GlobalKey(debugLabel: 'A');
double value = 0.0;
Widget buildSlider(int parents) {
Widget createParents(int parents, StateSetter setState) {
Widget slider = Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
);
for (int i = 0; i < parents; ++i) {
slider = Column(children: <Widget>[slider]);
}
return slider;
}
return Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: createParents(parents, setState),
),
);
},
),
);
}
Future<void> testReparenting(bool reparent) async {
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
final Offset center = tester.getCenter(find.byType(Slider));
// Move to 0.0.
TestGesture gesture = await tester.startGesture(Offset.zero);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..circle(x: 25.0, y: 24.0, radius: 1.0)
..circle(x: 212.5, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 1.0)
..circle(x: 587.5, y: 24.0, radius: 1.0)
..circle(x: 775.0, y: 24.0, radius: 1.0)
..circle(x: 24.0, y: 24.0, radius: 10.0),
);
gesture = await tester.startGesture(center);
await tester.pump();
// Wait for animations to start.
await tester.pump(const Duration(milliseconds: 25));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
expect(
sliderBox,
paints
..circle(x: 111.20703125, y: 24.0, radius: 5.687664985656738)
..circle(x: 25.0, y: 24.0, radius: 1.0)
..circle(x: 212.5, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 1.0)
..circle(x: 587.5, y: 24.0, radius: 1.0)
..circle(x: 775.0, y: 24.0, radius: 1.0)
..circle(x: 111.20703125, y: 24.0, radius: 10.0),
);
// Reparenting in the middle of an animation should do nothing.
if (reparent) {
await tester.pumpWidget(buildSlider(2));
}
// Move a little further in the animations.
await tester.pump(const Duration(milliseconds: 10));
expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
expect(
sliderBox,
paints
..circle(x: 190.0135726928711, y: 24.0, radius: 12.0)
..circle(x: 25.0, y: 24.0, radius: 1.0)
..circle(x: 212.5, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 1.0)
..circle(x: 587.5, y: 24.0, radius: 1.0)
..circle(x: 775.0, y: 24.0, radius: 1.0)
..circle(x: 190.0135726928711, y: 24.0, radius: 10.0),
);
// Wait for animations to finish.
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..circle(x: 400.0, y: 24.0, radius: 24.0)
..circle(x: 25.0, y: 24.0, radius: 1.0)
..circle(x: 212.5, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 1.0)
..circle(x: 587.5, y: 24.0, radius: 1.0)
..circle(x: 775.0, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 10.0),
);
await gesture.up();
await tester.pumpAndSettle();
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
expect(
sliderBox,
paints
..circle(x: 25.0, y: 24.0, radius: 1.0)
..circle(x: 212.5, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 1.0)
..circle(x: 587.5, y: 24.0, radius: 1.0)
..circle(x: 775.0, y: 24.0, radius: 1.0)
..circle(x: 400.0, y: 24.0, radius: 10.0),
);
}
await tester.pumpWidget(buildSlider(1));
// Do it once without reparenting in the middle of an animation
await testReparenting(false);
// Now do it again with reparenting in the middle of an animation.
await testReparenting(true);
});
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Slider(
value: 0.5,
onChanged: (double v) { },
),
),
),
));
expect(
semantics,
hasSemantics(
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
]),
ignoreRect: true,
ignoreTransform: true,
));
// Disable slider
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: const Material(
child: Slider(
value: 0.5,
onChanged: null,
),
),
),
));
expect(
semantics,
hasSemantics(
TestSemantics.root(),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('Slider Semantics - iOS', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
Theme(
data: ThemeData.light().copyWith(
platform: TargetPlatform.iOS,
),
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Slider(
value: 100.0,
min: 0.0,
max: 200.0,
onChanged: (double v) { },
),
),
),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: '50%',
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
]),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Slider(
value: 40.0,
min: 0.0,
max: 200.0,
divisions: 10,
semanticFormatterCallback: (double value) => value.round().toString(),
onChanged: (double v) { },
),
),
),
));
expect(
semantics,
hasSemantics(
TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: '40',
increasedValue: '60',
decreasedValue: '20',
textDirection: TextDirection.ltr,
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
]),
ignoreRect: true,
ignoreTransform: true,
));
semantics.dispose();
});
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {
final ThemeData baseTheme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
);
SliderThemeData theme = baseTheme.sliderTheme;
double value = 0.45;
Widget buildApp({ SliderThemeData sliderTheme, int divisions, bool enabled = true }) {
final ValueChanged<double> onChanged = enabled ? (double d) => value = d : null;
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Theme(
data: baseTheme,
child: SliderTheme(
data: sliderTheme,
child: Slider(
value: value,
label: '$value',
divisions: divisions,
onChanged: onChanged,
),
),
),
),
),
),
);
}
Future<void> expectValueIndicator({
bool isVisible,
SliderThemeData theme,
int divisions,
bool enabled = true,
}) async {
// Discrete enabled widget.
await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled));
final Offset center = tester.getCenter(find.byType(Slider));
final TestGesture gesture = await tester.startGesture(center);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(Slider));
expect(
sliderBox,
isVisible
? (paints..path(color: theme.valueIndicatorColor))
: isNot(paints..path(color: theme.valueIndicatorColor)),
);
await gesture.up();
}
// Default (showValueIndicator set to onlyForDiscrete).
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// With showValueIndicator set to onlyForContinuous.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onlyForContinuous);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// discrete enabled widget with showValueIndicator set to always.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.always);
await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: true, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
// discrete enabled widget with showValueIndicator set to never.
theme = theme.copyWith(showValueIndicator: ShowValueIndicator.never);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false);
await expectValueIndicator(isVisible: false, theme: theme, enabled: true);
await expectValueIndicator(isVisible: false, theme: theme, enabled: false);
});
testWidgets("Slider doesn't start any animations after dispose", (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
);
},
),
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pumpAndSettle(const Duration(milliseconds: 100));
expect(value, equals(0.5));
await gesture.moveBy(const Offset(-500.0, 0.0));
await tester.pumpAndSettle(const Duration(milliseconds: 100));
// Change the tree to dispose the original widget.
await tester.pumpWidget(Container());
expect(await tester.pumpAndSettle(const Duration(milliseconds: 100)), equals(1));
await gesture.up();
});
testWidgets('Slider.adaptive', (WidgetTester tester) async {
double value = 0.5;
Widget buildFrame(TargetPlatform platform) {
return MaterialApp(
theme: ThemeData(platform: platform),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Slider.adaptive(
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
);
}
await tester.pumpWidget(buildFrame(TargetPlatform.iOS));
expect(find.byType(Slider), findsOneWidget);
expect(find.byType(CupertinoSlider), findsOneWidget);
expect(value, 0.5);
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CupertinoSlider)));
// Drag to the right end of the track.
await gesture.moveBy(const Offset(600.0, 0.0));
expect(value, 1.0);
value = 0.5;
await tester.pumpWidget(buildFrame(TargetPlatform.android));
await tester.pumpAndSettle(); // Finish the theme change animation.
expect(find.byType(Slider), findsOneWidget);
expect(find.byType(CupertinoSlider), findsNothing);
expect(value, 0.5);
gesture = await tester.startGesture(tester.getCenter(find.byType(Slider)));
// Drag to the right end of the track.
await gesture.moveBy(const Offset(600.0, 0.0));
expect(value, 1.0);
});
testWidgets('Slider respects height from theme', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
final SliderThemeData sliderTheme = SliderTheme.of(context).copyWith(tickMarkShape: TallSliderTickMarkShape());
return MediaQuery(
data: MediaQueryData.fromWindow(window),
child: Material(
child: Center(
child: IntrinsicHeight(
child: SliderTheme(
data: sliderTheme,
child: Slider(
key: sliderKey,
value: value,
divisions: 4,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
),
),
),
);
},
),
),
);
final RenderBox renderObject = tester.renderObject<RenderBox>(find.byType(Slider));
expect(renderObject.size.height, 200);
});
testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const Slider(
activeColor: Colors.blue,
divisions: 10,
inactiveColor: Colors.grey,
label: 'Set a value',
max: 100.0,
min: 0.0,
onChanged: null,
onChangeEnd: null,
onChangeStart: null,
semanticFormatterCallback: null,
value: 50.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>[
'value: 50.0',
'disabled',
'min: 0.0',
'max: 100.0',
'divisions: 10',
'label: "Set a value"',
'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
]);
});
}