blob: 4523c5153e651fce089091ef3cbce2fed435de96 [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 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
const CupertinoDynamicColor _kSystemFill = CupertinoDynamicColor(
color: Color.fromARGB(51, 120, 120, 128),
darkColor: Color.fromARGB(91, 120, 120, 128),
highContrastColor: Color.fromARGB(71, 120, 120, 128),
darkHighContrastColor: Color.fromARGB(112, 120, 120, 128),
elevatedColor: Color.fromARGB(51, 120, 120, 128),
darkElevatedColor: Color.fromARGB(91, 120, 120, 128),
highContrastElevatedColor: Color.fromARGB(71, 120, 120, 128),
darkHighContrastElevatedColor: Color.fromARGB(112, 120, 120, 128),
);
void main() {
Future<void> dragSlider(WidgetTester tester, Key sliderKey) {
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
return tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
}
testWidgets('Slider does not move when tapped (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey), warnIfMissed: false);
expect(value, equals(0.0));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider does not move when tapped (RTL)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
expect(value, equals(0.0));
await tester.tap(find.byKey(sliderKey), warnIfMissed: false);
expect(value, equals(0.0));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider calls onChangeStart once when interaction begins', (
WidgetTester tester,
) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
int numberOfTimesOnChangeStartIsCalled = 0;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
onChangeStart: (double value) {
numberOfTimesOnChangeStartIsCalled++;
},
),
),
);
},
),
),
),
);
await dragSlider(tester, sliderKey);
expect(numberOfTimesOnChangeStartIsCalled, equals(1));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider calls onChangeEnd once after interaction has ended', (
WidgetTester tester,
) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
int numberOfTimesOnChangeEndIsCalled = 0;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
onChangeEnd: (double value) {
numberOfTimesOnChangeEndIsCalled++;
},
),
),
);
},
),
),
),
);
await dragSlider(tester, sliderKey);
expect(numberOfTimesOnChangeEndIsCalled, equals(1));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
late double startValue;
late double endValue;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
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));
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
await tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0));
final Size size = tester.getSize(find.byKey(sliderKey));
final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius));
expect(startValue, equals(0.0));
expect(value, equals(finalValue));
expect(endValue, equals(finalValue));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets(
'Slider emits haptic feedback when hitting edge',
(WidgetTester tester) async {
final List<MethodCall> hapticLog = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
MethodCall methodCall,
) async {
hapticLog.add(methodCall);
return null;
});
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
);
// No haptic feedback should be emitted when the slider is created.
expect(hapticLog, hasLength(0));
const double unit = CupertinoThumbPainter.radius;
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
Offset thumbCenter = topLeft + const Offset(unit, unit);
const Offset delta = Offset(50.0, 0.0);
await tester.dragFrom(thumbCenter, delta);
await tester.pump();
thumbCenter += delta;
// No haptic feedback should be emitted when the slider is moved.
expect(hapticLog, hasLength(0));
// Move the slider to the end quickly.
await tester.timedDragFrom(
thumbCenter,
const Offset(1000.0, 0.0),
const Duration(milliseconds: 100),
);
// Medium haptic feedback should be emitted when the slider is quickly moved to the end.
expect(hapticLog, hasLength(1));
expect(
hapticLog.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.mediumImpact'),
);
// Move the slider to the start slowly.
thumbCenter = tester.getTopRight(find.byKey(sliderKey)) - const Offset(unit, -unit);
await tester.timedDragFrom(
thumbCenter,
-Offset(thumbCenter.dx - topLeft.dx - unit * 2, 0),
const Duration(milliseconds: 1100),
);
expect(value, equals(0.0));
// Selection click should be emitted when the slider is slowly moved to the start.
expect(hapticLog, hasLength(2));
expect(
hapticLog.last,
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
);
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets(
'Slider does not emit haptic feedback on non-iOS platforms',
(WidgetTester tester) async {
final List<MethodCall> hapticLog = <MethodCall>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
MethodCall methodCall,
) async {
hapticLog.add(methodCall);
return null;
});
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
);
const double unit = CupertinoThumbPainter.radius;
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
final Offset thumbCenter = topLeft + const Offset(unit, unit);
// Move the slider to the end.
await tester.dragFrom(thumbCenter, const Offset(1000.0, 0.0));
expect(value, equals(1.0));
expect(hapticLog, hasLength(0));
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.iOS}),
);
testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
late double startValue;
late double endValue;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
onChangeStart: (double value) {
setState(() {
startValue = value;
});
},
onChangeEnd: (double value) {
setState(() {
endValue = value;
});
},
),
),
);
},
),
),
),
);
expect(value, equals(0.0));
final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey));
const double unit = CupertinoThumbPainter.radius;
const double delta = 3.0 * unit;
await tester.dragFrom(bottomRight - const Offset(unit, unit), const Offset(-delta, 0.0));
final Size size = tester.getSize(find.byKey(sliderKey));
final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius));
expect(startValue, equals(0.0));
expect(value, equals(finalValue));
expect(endValue, equals(finalValue));
await tester.pump(); // No animation should start.
// Check the transientCallbackCount before tearing down the widget to ensure
// that no animation is running.
expect(SchedulerBinding.instance.transientCallbackCount, equals(0));
});
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: CupertinoSlider(value: 0.5, onChanged: (double v) {}),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
value: '50%',
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isSlider],
actions: SemanticsAction.decrease.index | SemanticsAction.increase.index,
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Disable slider
await tester.pumpWidget(
const MediaQuery(
data: MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: CupertinoSlider(value: 0.5, onChanged: null),
),
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(id: 1, flags: <SemanticsFlag>[SemanticsFlag.isSlider]),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Slider Semantics can be updated', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
double value = 0.5;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: CupertinoSlider(value: value, onChanged: (double v) {}),
),
),
);
expect(
tester.getSemantics(find.byType(CupertinoSlider)),
matchesSemantics(
isSlider: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '50%',
increasedValue: '60%',
decreasedValue: '40%',
textDirection: TextDirection.ltr,
),
);
value = 0.6;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: CupertinoSlider(value: value, onChanged: (double v) {}),
),
),
);
expect(
tester.getSemantics(find.byType(CupertinoSlider)),
matchesSemantics(
isSlider: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '60%',
increasedValue: '70%',
decreasedValue: '50%',
textDirection: TextDirection.ltr,
),
);
handle.dispose();
});
testWidgets('Slider respects themes', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(child: CupertinoSlider(onChanged: (double value) {}, value: 0.5)),
),
);
expect(
find.byType(CupertinoSlider),
// First line it paints is blue.
paints..rrect(color: CupertinoColors.systemBlue.color),
);
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: Center(child: CupertinoSlider(onChanged: (double value) {}, value: 0.5)),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: CupertinoColors.systemBlue.darkColor),
);
});
testWidgets('Themes can be overridden', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoSlider(
activeColor: CupertinoColors.activeGreen,
onChanged: (double value) {},
value: 0.5,
),
),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: CupertinoColors.systemGreen.darkColor),
);
});
testWidgets('Themes can be overridden by dynamic colors', (WidgetTester tester) async {
const CupertinoDynamicColor activeColor = CupertinoDynamicColor(
color: Color(0x00000001),
darkColor: Color(0x00000002),
elevatedColor: Color(0x00000003),
highContrastColor: Color(0x00000004),
darkElevatedColor: Color(0x00000005),
darkHighContrastColor: Color(0x00000006),
highContrastElevatedColor: Color(0x00000007),
darkHighContrastElevatedColor: Color(0x00000008),
);
Widget withTraits(
Brightness brightness,
CupertinoUserInterfaceLevelData level,
bool highContrast,
) {
return CupertinoTheme(
data: CupertinoThemeData(brightness: brightness),
child: CupertinoUserInterfaceLevel(
data: level,
child: MediaQuery(
data: MediaQueryData(highContrast: highContrast),
child: Center(
child: CupertinoSlider(
activeColor: activeColor,
onChanged: (double value) {},
value: 0.5,
),
),
),
),
);
}
await tester.pumpWidget(
CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.base, false)),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.color));
await tester.pumpWidget(
CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.base, false)),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkColor));
await tester.pumpWidget(
CupertinoApp(
home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.elevated, false),
),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkElevatedColor));
await tester.pumpWidget(
CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.base, true)),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkHighContrastColor));
await tester.pumpWidget(
CupertinoApp(
home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.elevated, true),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: activeColor.darkHighContrastElevatedColor),
);
await tester.pumpWidget(
CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.base, true)),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.highContrastColor));
await tester.pumpWidget(
CupertinoApp(
home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.elevated, false),
),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.elevatedColor));
await tester.pumpWidget(
CupertinoApp(
home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.elevated, true),
),
);
expect(
find.byType(CupertinoSlider),
paints..rrect(color: activeColor.highContrastElevatedColor),
);
});
testWidgets('track color is dynamic', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.light),
home: Center(
child: CupertinoSlider(
activeColor: CupertinoColors.activeGreen,
onChanged: (double value) {},
value: 0,
),
),
),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: _kSystemFill.color));
expect(find.byType(CupertinoSlider), isNot(paints..rrect(color: _kSystemFill.darkColor)));
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: Center(
child: CupertinoSlider(
activeColor: CupertinoColors.activeGreen,
onChanged: (double value) {},
value: 0,
),
),
),
);
expect(find.byType(CupertinoSlider), paints..rrect(color: _kSystemFill.darkColor));
expect(find.byType(CupertinoSlider), isNot(paints..rrect(color: _kSystemFill.color)));
});
testWidgets('Thumb color can be overridden', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoSlider(
thumbColor: CupertinoColors.systemPurple,
onChanged: (double value) {},
value: 0,
),
),
),
);
expect(
find.byType(CupertinoSlider),
paints
..rrect()
..rrect()
..rrect()
..rrect()
..rrect()
..rrect(color: CupertinoColors.systemPurple.color),
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoSlider(
thumbColor: CupertinoColors.activeOrange,
onChanged: (double value) {},
value: 0,
),
),
),
);
expect(
find.byType(CupertinoSlider),
paints
..rrect()
..rrect()
..rrect()
..rrect()
..rrect()
..rrect(color: CupertinoColors.activeOrange.color),
);
});
testWidgets('Hovering over Cupertino slider thumb updates cursor to clickable on Web', (
WidgetTester tester,
) async {
final Key sliderKey = UniqueKey();
double value = 0.0;
await tester.pumpWidget(
CupertinoApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: CupertinoSlider(
key: sliderKey,
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
),
),
);
},
),
),
),
);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: const Offset(10, 10));
await tester.pumpAndSettle();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey));
await gesture.moveTo(topLeft + const Offset(15, 0));
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
}