blob: 02a5cab358f88a9653d29b2bc4a27a9e7b403af5 [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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/src/physics/utils.dart' show nearEqual;
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
void main() {
// Regression test for https://github.com/flutter/flutter/issues/105833
testWidgets('Drag gesture uses provided gesture settings', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.1, 0.5);
bool dragStarted = false;
final Key sliderKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.deferToChild,
onHorizontalDragStart: (DragStartDetails details) {
dragStarted = true;
},
child: MediaQuery(
data: MediaQuery.of(context).copyWith(gestureSettings: const DeviceGestureSettings(touchSlop: 20)),
child: RangeSlider(
key: sliderKey,
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
),
);
},
),
),
),
);
TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
expect(values, const RangeValues(0.1, 0.5));
expect(dragStarted, true);
dragStarted = false;
await drag.up();
await tester.pumpAndSettle();
drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey)));
await tester.pump(kPressTimeout);
bool sliderEnd = false;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: GestureDetector(
behavior: HitTestBehavior.deferToChild,
onHorizontalDragStart: (DragStartDetails details) {
dragStarted = true;
},
child: MediaQuery(
data: MediaQuery.of(context).copyWith(gestureSettings: const DeviceGestureSettings(touchSlop: 10)),
child: RangeSlider(
key: sliderKey,
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
onChangeEnd: (RangeValues newValues) {
sliderEnd = true;
},
),
),
),
),
);
},
),
),
),
);
// More than touch slop.
await drag.moveBy(const Offset(12.0, 0));
await drag.up();
await tester.pumpAndSettle();
expect(sliderEnd, true);
expect(dragStarted, false);
});
testWidgets('Range Slider can move when tapped (continuous LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
// boundaries
expect(values, equals(const RangeValues(0.3, 0.7)));
// taps at 0.5
await tester.tap(find.byType(RangeSlider));
await tester.pump();
expect(values, equals(const RangeValues(0.3, 0.7)));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// The start thumb is selected when tapping the left inactive track.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1;
await tester.tapAt(leftTarget);
expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01));
expect(values.end, equals(0.7));
// The end thumb is selected when tapping the right inactive track.
await tester.pump();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9;
await tester.tapAt(rightTarget);
expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01));
expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01));
});
testWidgets('Range Slider can move when tapped (continuous RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
// boundaries
expect(values, equals(const RangeValues(0.3, 0.7)));
// taps at 0.5
await tester.tap(find.byType(RangeSlider));
await tester.pump();
expect(values, equals(const RangeValues(0.3, 0.7)));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// The end thumb is selected when tapping the left inactive track.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1;
await tester.tapAt(leftTarget);
expect(values.start, 0.3);
expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01));
// The start thumb is selected when tapping the right inactive track.
await tester.pump();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9;
await tester.tapAt(rightTarget);
expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01));
expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01));
});
testWidgets('Range Slider can move when tapped (discrete LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
// boundaries
expect(values, equals(const RangeValues(30, 70)));
// taps at 0.5
await tester.tap(find.byType(RangeSlider));
await tester.pumpAndSettle();
expect(values, equals(const RangeValues(30, 70)));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// The start thumb is selected when tapping the left inactive track.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1;
await tester.tapAt(leftTarget);
await tester.pumpAndSettle();
expect(values.start.round(), equals(10));
expect(values.end.round(), equals(70));
// The end thumb is selected when tapping the right inactive track.
await tester.pump();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9;
await tester.tapAt(rightTarget);
await tester.pumpAndSettle();
expect(values.start.round(), equals(10));
expect(values.end.round(), equals(90));
});
testWidgets('Range Slider can move when tapped (discrete RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// No thumbs get select when tapping between the thumbs outside the touch
// boundaries
expect(values, equals(const RangeValues(30, 70)));
// taps at 0.5
await tester.tap(find.byType(RangeSlider));
await tester.pumpAndSettle();
expect(values, equals(const RangeValues(30, 70)));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// The start thumb is selected when tapping the left inactive track.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1;
await tester.tapAt(leftTarget);
await tester.pumpAndSettle();
expect(values.start.round(), equals(30));
expect(values.end.round(), equals(90));
// The end thumb is selected when tapping the right inactive track.
await tester.pump();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9;
await tester.tapAt(rightTarget);
await tester.pumpAndSettle();
expect(values.start.round(), equals(10));
expect(values.end.round(), equals(90));
});
testWidgets('Range Slider thumbs can be dragged to the min and max (continuous LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the start thumb to the min.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4);
expect(values.start, equals(0));
// Drag the end thumb to the max.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4);
expect(values.end, equals(1));
});
testWidgets('Range Slider thumbs can be dragged to the min and max (continuous RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the end thumb to the max.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4);
expect(values.end, equals(1));
// Drag the start thumb to the min.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4);
expect(values.start, equals(0));
});
testWidgets('Range Slider thumbs can be dragged to the min and max (discrete LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the start thumb to the min.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4);
expect(values.start, equals(0));
// Drag the end thumb to the max.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4);
expect(values.end, equals(100));
});
testWidgets('Range Slider thumbs can be dragged to the min and max (discrete RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the end thumb to the max.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4);
expect(values.end, equals(100));
// Drag the start thumb to the min.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4);
expect(values.start, equals(0));
});
testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the start thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the end thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the start thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3);
expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05));
});
testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the end thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the start thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the start thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3);
expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05));
});
testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the start thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.start, moreOrLessEquals(50, epsilon: 0.01));
// Drag the end thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.end, moreOrLessEquals(50, epsilon: 0.01));
// Drag the start thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3);
expect(values.start, moreOrLessEquals(20, epsilon: 0.01));
});
testWidgets('Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the end thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.end, moreOrLessEquals(50, epsilon: 0.01));
// Drag the start thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, moreOrLessEquals(50, epsilon: 0.01));
// Drag the start thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3);
expect(values.start, moreOrLessEquals(20, epsilon: 0.01));
});
testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the start thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the end thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the end thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3);
expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05));
});
testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the end thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the start thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05));
// Drag the end thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3);
expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05));
});
testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete LTR)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the start thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.start, moreOrLessEquals(50, epsilon: 0.01));
// Drag the end thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.end, moreOrLessEquals(50, epsilon: 0.01));
// Drag the end thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3);
expect(values.end, moreOrLessEquals(80, epsilon: 0.01));
});
testWidgets('Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete RTL)', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
divisions: 10,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the end thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
expect(values.end, moreOrLessEquals(50, epsilon: 0.01));
// Drag the start thumb towards the center.
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, moreOrLessEquals(50, epsilon: 0.01));
// Drag the end thumb apart.
await tester.pumpAndSettle();
await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3);
expect(values.end, moreOrLessEquals(80, epsilon: 0.01));
});
testWidgets('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by tap', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
RangeValues? startValues;
RangeValues? endValues;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
onChangeStart: (RangeValues newValues) {
startValues = newValues;
},
onChangeEnd: (RangeValues newValues) {
endValues = newValues;
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the start thumb towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
expect(startValues, null);
expect(endValues, null);
await tester.dragFrom(leftTarget, (bottomRight - topLeft) * 0.2);
expect(startValues!.start, moreOrLessEquals(30, epsilon: 1));
expect(startValues!.end, moreOrLessEquals(70, epsilon: 1));
expect(values.start, moreOrLessEquals(50, epsilon: 1));
expect(values.end, moreOrLessEquals(70, epsilon: 1));
expect(endValues!.start, moreOrLessEquals(50, epsilon: 1));
expect(endValues!.end, moreOrLessEquals(70, epsilon: 1));
});
testWidgets('Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by drag', (WidgetTester tester) async {
RangeValues values = const RangeValues(30, 70);
late RangeValues startValues;
late RangeValues endValues;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
onChangeStart: (RangeValues newValues) {
startValues = newValues;
},
onChangeEnd: (RangeValues newValues) {
endValues = newValues;
},
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
// Drag the thumbs together.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, (bottomRight - topLeft) * 0.2);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, (bottomRight - topLeft) * -0.2);
await tester.pumpAndSettle();
expect(values.start, moreOrLessEquals(50, epsilon: 1));
expect(values.end, moreOrLessEquals(51, epsilon: 1));
// Drag the end thumb to the right.
final Offset middleTarget = topLeft + (bottomRight - topLeft) * 0.5;
await tester.dragFrom(middleTarget, (bottomRight - topLeft) * 0.4);
await tester.pumpAndSettle();
expect(startValues.start, moreOrLessEquals(50, epsilon: 1));
expect(startValues.end, moreOrLessEquals(51, epsilon: 1));
expect(endValues.start, moreOrLessEquals(50, epsilon: 1));
expect(endValues.end, moreOrLessEquals(90, epsilon: 1));
});
ThemeData buildTheme() {
return 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),
),
);
}
Widget buildThemedApp({
required ThemeData theme,
Color? activeColor,
Color? inactiveColor,
int? divisions,
bool enabled = true,
}) {
RangeValues values = const RangeValues(0.5, 0.75);
final ValueChanged<RangeValues>? onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
divisions: divisions,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
),
),
),
),
),
);
}
testWidgets('Range Slider uses the right theme colors for the right shapes for a default enabled slider', (WidgetTester tester) async {
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(theme: theme));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
// Check default theme for enabled widget.
expect(
sliderBox,
paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: sliderTheme.activeTrackColor)
..rrect(color: sliderTheme.inactiveTrackColor),
);
expect(
sliderBox,
paints
..circle(color: sliderTheme.thumbColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes when setting the active color', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(theme: theme, activeColor: activeColor));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: activeColor)
..rrect(color: sliderTheme.inactiveTrackColor),
);
expect(
sliderBox,
paints
..circle(color: activeColor)
..circle(color: activeColor),
);
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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes when setting the inactive color', (WidgetTester tester) async {
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(theme: theme, inactiveColor: inactiveColor));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: sliderTheme.activeTrackColor)
..rrect(color: inactiveColor),
);
expect(
sliderBox,
paints
..circle(color: sliderTheme.thumbColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: activeColor)
..rrect(color: inactiveColor),
);
expect(
sliderBox,
paints
..circle(color: activeColor)
..circle(color: activeColor),
);
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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider', (WidgetTester tester) async {
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(theme: theme, divisions: 3));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.inactiveTrackColor)
..rect(color: sliderTheme.activeTrackColor)
..rrect(color: sliderTheme.inactiveTrackColor),
);
expect(
sliderBox,
paints
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.activeTickMarkColor)
..circle(color: sliderTheme.inactiveTickMarkColor)
..circle(color: sliderTheme.thumbColor)
..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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
divisions: 3,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: inactiveColor)
..rect(color: activeColor)
..rrect(color: inactiveColor),
);
expect(
sliderBox,
paints
..circle(color: activeColor)
..circle(color: activeColor)
..circle(color: inactiveColor)
..circle(color: activeColor)
..circle(color: activeColor)
..circle(color: activeColor),
);
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)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a default disabled slider', (WidgetTester tester) async {
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(theme: theme, enabled: false));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.disabledInactiveTrackColor)
..rect(color: sliderTheme.disabledActiveTrackColor)
..rrect(color: sliderTheme.disabledInactiveTrackColor),
);
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes for a disabled slider with active and inactive colors', (WidgetTester tester) async {
const Color activeColor = Color(0xcafefeed);
const Color inactiveColor = Color(0xdeadbeef);
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(buildThemedApp(
theme: theme,
activeColor: activeColor,
inactiveColor: inactiveColor,
enabled: false,
));
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
expect(
sliderBox,
paints
..rrect(color: sliderTheme.disabledInactiveTrackColor)
..rect(color: sliderTheme.disabledActiveTrackColor)
..rrect(color: sliderTheme.disabledInactiveTrackColor),
);
expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor)));
expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor)));
});
testWidgets('Range Slider uses the right theme colors for the right shapes when the value indicators are showing', (WidgetTester tester) async {
final ThemeData theme = buildTheme();
final SliderThemeData sliderTheme = theme.sliderTheme;
RangeValues values = const RangeValues(0.5, 0.75);
Widget buildApp({
Color? activeColor,
Color? inactiveColor,
int? divisions,
bool enabled = true,
}) {
final ValueChanged<RangeValues>? onChanged = !enabled ? null : (RangeValues newValues) {
values = newValues;
};
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
divisions: divisions,
activeColor: activeColor,
inactiveColor: inactiveColor,
onChanged: onChanged,
),
),
),
),
),
);
}
await tester.pumpWidget(buildApp(divisions: 3));
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0);
final TestGesture gesture = await tester.startGesture(topRight);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(values.end, equals(1));
expect(
valueIndicatorBox,
paints
..path(color: Colors.black) // shadow
..path(color: Colors.black) // shadow
..path(color: sliderTheme.valueIndicatorColor)
..paragraph()
);
await gesture.up();
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
});
testWidgets('Range Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.5, 0.75);
const Color fillColor = Color(0xf55f5f5f);
Widget buildApp({
Color? activeColor,
Color? inactiveColor,
int? divisions,
bool enabled = true,
}) {
void onChanged(RangeValues newValues) {
values = newValues;
}
return MaterialApp(
home: Scaffold(
// The builder is used to pass the context from the MaterialApp widget
// to the [Navigator]. This context is required in order for the
// Navigator to work.
body: Builder(
builder: (BuildContext context) {
return Column(
children: <Widget>[
RangeSlider(
values: values,
labels: RangeLabels(
values.start.toStringAsFixed(2),
values.end.toStringAsFixed(2),
),
divisions: divisions,
onChanged: onChanged,
),
ElevatedButton(
child: const Text('Next'),
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return ElevatedButton(
child: const Text('Inner page'),
onPressed: () { Navigator.of(context).pop(); },
);
},
),
);
},
),
],
);
},
),
),
);
}
await tester.pumpWidget(buildApp(divisions: 3));
final RenderObject valueIndicatorBox = tester.renderObject(find.byType(Overlay));
final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0);
final TestGesture gesture = await tester.startGesture(topRight);
// Wait for value indicator animation to finish.
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints
// Represents the raised button wth next text.
..path(color: Colors.black)
..paragraph()
// Represents the range slider.
..path(color: fillColor)
..paragraph()
..path(color: fillColor)
..paragraph(),
);
// Represents the Raised Button and Range Slider.
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 6));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 3));
await tester.tap(find.text('Next'));
await tester.pumpAndSettle();
expect(find.byType(RangeSlider), findsNothing);
expect(
valueIndicatorBox,
isNot(
paints
..path(color: fillColor)
..paragraph()
..path(color: fillColor)
..paragraph(),
),
);
// Represents the raised button with inner page text.
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2));
expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 1));
// Don't stop holding the value indicator.
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('Range Slider top thumb gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
thumbColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
);
},
),
),
),
);
final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03));
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03));
await tester.pumpAndSettle();
expect(
sliderBox,
paints
..circle(color: sliderTheme.thumbColor)
..circle(color: sliderTheme.overlappingShapeStrokeColor)
..circle(color: sliderTheme.thumbColor),
);
});
testWidgets('Range Slider top value indicator gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
showValueIndicator: ShowValueIndicator.always,
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
);
},
),
),
),
);
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03));
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints
..path(color: Colors.black) // shadow
..path(color: Colors.black) // shadow
..path(color: sliderTheme.valueIndicatorColor)
..paragraph(),
);
await gesture.up();
});
testWidgets('Range Slider top value indicator gets stroked when overlapping with large text scale', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
overlappingShapeStrokeColor: Color(0xff000002),
showValueIndicator: ShowValueIndicator.always,
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return MediaQuery(
data: const MediaQueryData(textScaleFactor: 2.0),
child: Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
),
);
},
),
),
),
);
final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay));
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03));
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
expect(
valueIndicatorBox,
paints
..path(color: Colors.black) // shadow
..path(color: Colors.black) // shadow
..path(color: sliderTheme.valueIndicatorColor)
..paragraph(),
);
await gesture.up();
});
testWidgets('Range Slider thumb gets stroked when overlapping', (WidgetTester tester) async {
RangeValues values = const RangeValues(0.3, 0.7);
final ThemeData theme = ThemeData(
platform: TargetPlatform.android,
primarySwatch: Colors.blue,
sliderTheme: const SliderThemeData(
valueIndicatorColor: Color(0xff000001),
showValueIndicator: ShowValueIndicator.onlyForContinuous,
),
);
final SliderThemeData sliderTheme = theme.sliderTheme;
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: Theme(
data: theme,
child: RangeSlider(
values: values,
labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)),
onChanged: (RangeValues newValues) {
setState(() {
values = newValues;
});
},
),
),
),
);
},
),
),
),
);
// Get the bounds of the track by finding the slider edges and translating
// inwards by the overlay radius.
final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0);
final Offset middle = topLeft + bottomRight / 2;
// Drag the thumbs towards the center.
final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3;
await tester.dragFrom(leftTarget, middle - leftTarget);
await tester.pumpAndSettle();
final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7;
await tester.dragFrom(rightTarget, middle - rightTarget);
await tester.pumpAndSettle();
expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03));
expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03));
final TestGesture gesture = await tester.startGesture(middle);
await tester.pumpAndSettle();
/// The first circle is the thumb, the second one is the overlapping shape
/// circle, and the last one is the second thumb.
expect(
find.byType(RangeSlider),
paints
..circle()
..circle(color: sliderTheme.overlappingShapeStrokeColor)
..circle(),
);
await gesture.up();
expect(
find.byType(RangeSlider),
paints
..circle()
..circle(color: sliderTheme.overlappingShapeStrokeColor)
..circle(),
);
});
// Regression test for https://github.com/flutter/flutter/issues/101868
testWidgets('RangeSlider.label info should not write to semantic node', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData.light(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: RangeSlider(
values: const RangeValues(10.0, 12.0),
max: 100.0,
onChanged: (RangeValues v) { },
labels: const RangeLabels('Begin', 'End'),
),
),
),
),
),
);
await tester.pumpAndSettle();
expect(
tester.getSemantics(find.byType(RangeSlider)),
matchesSemantics(
scopesRoute: true,
children:<Matcher>[
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '10%',
decreasedValue: '5%',
label: ''
),
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '12%',
increasedValue: '17%',
decreasedValue: '12%',
label: ''
),
],
),
],
),
);
});
testWidgets('Range Slider Semantics - ltr', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData.light(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: RangeSlider(
values: const RangeValues(10.0, 30.0),
max: 100.0,
onChanged: (RangeValues v) { },
),
),
),
),
),
);
await tester.pumpAndSettle();
final SemanticsNode semanticsNode = tester.getSemantics(find.byType(RangeSlider));
expect(
semanticsNode,
matchesSemantics(
scopesRoute: true,
children:<Matcher>[
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0),
),
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0),
),
],
),
],
),
);
// TODO(tahatesser): This is a workaround for matching
// the semantics node rects by avoiding floating point errors.
// https://github.com/flutter/flutter/issues/115079
// Get semantics node rects.
final List<Rect> rects = <Rect>[];
semanticsNode.visitChildren((SemanticsNode node) {
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
return true;
});
return true;
});
// Test that the semantics node rect sizes are correct.
expect(rects, <Rect>[
const Rect.fromLTRB(75.0, 276.0, 123.0, 324.0),
const Rect.fromLTRB(226.0, 276.0, 274.0, 324.0)
]);
});
testWidgets('Range Slider Semantics - rtl', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Theme(
data: ThemeData.light(),
child: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: RangeSlider(
values: const RangeValues(10.0, 30.0),
max: 100.0,
onChanged: (RangeValues v) { },
),
),
),
),
),
);
await tester.pumpAndSettle();
final SemanticsNode semanticsNode = tester.getSemantics(find.byType(RangeSlider));
expect(
semanticsNode,
matchesSemantics(
scopesRoute: true,
children:<Matcher>[
matchesSemantics(
children: <Matcher>[
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '10%',
increasedValue: '15%',
decreasedValue: '5%',
),
matchesSemantics(
isEnabled: true,
isSlider: true,
hasEnabledState: true,
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '30%',
increasedValue: '35%',
decreasedValue: '25%',
),
],
),
],
),
);
// TODO(tahatesser): This is a workaround for matching
// the semantics node rects by avoiding floating point errors.
// https://github.com/flutter/flutter/issues/115079
// Get semantics node rects.
final List<Rect> rects = <Rect>[];
semanticsNode.visitChildren((SemanticsNode node) {
node.visitChildren((SemanticsNode node) {
// Round rect values to avoid floating point errors.
rects.add(
Rect.fromLTRB(
node.rect.left.roundToDouble(),
node.rect.top.roundToDouble(),
node.rect.right.roundToDouble(),
node.rect.bottom.roundToDouble(),
),
);
return true;
});
return true;
});
// Test that the semantics node rect sizes are correct.
expect(rects, <Rect>[
const Rect.fromLTRB(526.0, 276.0, 574.0, 324.0),
const Rect.fromLTRB(677.0, 276.0, 725.0, 324.0)
]);
});
testWidgets('Range Slider implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RangeSlider(
activeColor: Colors.blue,
divisions: 4,
inactiveColor: Colors.grey,
labels: const RangeLabels('lowerValue', 'upperValue'),
max: 100.0,
onChanged: null,
values: const RangeValues(25.0, 75.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>[
'valueStart: 25.0',
'valueEnd: 75.0',
'disabled',
'min: 0.0',
'max: 100.0',
'divisions: 4',
'labelStart: "lowerValue"',
'labelEnd: "upperValue"',
'activeColor: MaterialColor(primary value: Color(0xff2196f3))',
'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))',
]);
});
testWidgets('Range Slider can be painted in a narrower constraint when track shape is RoundedRectRange', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: SizedBox(
height: 10.0,
width: 0.0,
child: RangeSlider(
values: const RangeValues(0.25, 0.5),
onChanged: null,
),
),
),
),
),
),
);
// _RenderRangeSlider is the last render object in the tree.
final RenderObject renderObject = tester.allRenderObjects.last;
expect(
renderObject,
paints
// left inactive track RRect
..rrect(rrect: RRect.fromLTRBAndCorners(-24.0, 3.0, -12.0, 7.0, topLeft: const Radius.circular(2.0), bottomLeft: const Radius.circular(2.0)))
// active track RRect
..rect(rect: const Rect.fromLTRB(-12.0, 2.0, 0.0, 8.0))
// right inactive track RRect
..rrect(rrect: RRect.fromLTRBAndCorners(0.0, 3.0, 24.0, 7.0, topRight: const Radius.circular(2.0), bottomRight: const Radius.circular(2.0)))
// thumbs
..circle(x: -12.0, y: 5.0, radius: 10.0)
..circle(x: 0.0, y: 5.0, radius: 10.0),
);
});
testWidgets('Range Slider can be painted in a narrower constraint when track shape is Rectangular', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(
sliderTheme: const SliderThemeData(
rangeTrackShape: RectangularRangeSliderTrackShape(),
),
),
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: SizedBox(
height: 10.0,
width: 0.0,
child: RangeSlider(
values: const RangeValues(0.25, 0.5),
onChanged: null,
),
),
),
),
),
),
);
// _RenderRangeSlider is the last render object in the tree.
final RenderObject renderObject = tester.allRenderObjects.last;
//There should no gap between the inactive track and active track.
expect(
renderObject,
paints
// left inactive track RRect
..rect(rect: const Rect.fromLTRB(-24.0, 3.0, -12.0, 7.0))
// active track RRect
..rect(rect: const Rect.fromLTRB(-12.0, 3.0, 0.0, 7.0))
// right inactive track RRect
..rect(rect: const Rect.fromLTRB(0.0, 3.0, 24.0, 7.0))
// thumbs
..circle(x: -12.0, y: 5.0, radius: 10.0)
..circle(x: 0.0, y: 5.0, radius: 10.0),
);
});
testWidgets('Update the divisions and values at the same time for RangeSlider', (WidgetTester tester) async {
// Regress test for https://github.com/flutter/flutter/issues/65943
Widget buildFrame(double maxValue) {
return MaterialApp(
home: Material(
child: Center(
child: RangeSlider(
values: const RangeValues(5, 8),
max: maxValue,
divisions: maxValue.toInt(),
onChanged: (RangeValues newValue) {},
),
),
),
);
}
await tester.pumpWidget(buildFrame(10));
// _RenderRangeSlider is the last render object in the tree.
final RenderObject renderObject = tester.allRenderObjects.last;
// Update the divisions from 10 to 15, the thumbs should be paint at the correct position.
await tester.pumpWidget(buildFrame(15));
await tester.pumpAndSettle(); // Finish the animation.
late Rect activeTrackRect;
expect(renderObject, paints..something((Symbol method, List<dynamic> arguments) {
if (method != #drawRect) {
return false;
}
activeTrackRect = arguments[0] as Rect;
return true;
}));
// The 1st thumb should at one-third(5 / 15) of the Slider.
// The 2nd thumb should at (8 / 15) of the Slider.
// The left of the active track shape is the position of the 1st thumb.
// The right of the active track shape is the position of the 2nd thumb.
// 24.0 is the default margin, (800.0 - 24.0 - 24.0) is the slider's width.
expect(nearEqual(activeTrackRect.left, (800.0 - 24.0 - 24.0) * (5 / 15) + 24.0, 0.01), true);
expect(nearEqual(activeTrackRect.right, (800.0 - 24.0 - 24.0) * (8 / 15) + 24.0, 0.01), true);
});
testWidgets('RangeSlider changes mouse cursor when hovered', (WidgetTester tester) async {
const RangeValues values = RangeValues(50, 70);
// Test default cursor.
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
values: values,
max: 100.0,
onChanged: (RangeValues values) {},
),
),
),
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(RangeSlider)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test custom cursor.
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
values: values,
max: 100.0,
mouseCursor: const MaterialStatePropertyAll<MouseCursor?>(SystemMouseCursors.text),
onChanged: (RangeValues values) {},
),
),
),
),
),
),
);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
});
testWidgets('RangeSlider MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async {
RangeValues values = const RangeValues(50, 70);
const MouseCursor disabledCursor = SystemMouseCursors.basic;
const MouseCursor hoveredCursor = SystemMouseCursors.grab;
const MouseCursor draggedCursor = SystemMouseCursors.move;
Widget buildFrame({ required bool enabled }) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: RangeSlider(
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return disabledCursor;
}
if (states.contains(MaterialState.dragged)) {
return draggedCursor;
}
if (states.contains(MaterialState.hovered)) {
return hoveredCursor;
}
return SystemMouseCursors.none;
},
),
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
onChangeStart: enabled ? (RangeValues newValues) {} : null,
onChangeEnd: enabled ? (RangeValues newValues) {} : null,
),
),
);
},
),
),
),
);
}
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: Offset.zero);
await tester.pumpWidget(buildFrame(enabled: false));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor);
await tester.pumpWidget(buildFrame(enabled: true));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none);
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); // start hover
await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor);
await tester.timedDrag(
find.byType(RangeSlider),
const Offset(20.0, 0.0),
const Duration(milliseconds: 100),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), draggedCursor);
});
testWidgets('RangeSlider can be hovered and has correct hover color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
final ThemeData theme = ThemeData();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not hovered.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start hovering.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider)));
// RangeSlider has overlay when enabled and hovered.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
// RangeSlider does not have an overlay when disabled and hovered.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
});
testWidgets('RangeSlider is draggable and has correct dragged color', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
final ThemeData theme = ThemeData();
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not dragged.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// RangeSlider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)),
);
});
testWidgets('RangeSlider overlayColor supports hovered and dragged states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
RangeValues values = const RangeValues(50, 70);
const Color hoverColor = Color(0xffff0000);
const Color draggedColor = Color(0xff0000ff);
Widget buildApp({bool enabled = true}) {
return MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Material(
child: Center(
child: RangeSlider(
values: values,
max: 100.0,
overlayColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoverColor;
}
if (states.contains(MaterialState.dragged)) {
return draggedColor;
}
return null;
}),
onChanged: enabled
? (RangeValues newValues) {
setState(() {
values = newValues;
});
}
: null,
onChangeStart: enabled ? (RangeValues newValues) {} : null,
onChangeEnd: enabled ? (RangeValues newValues) {} : null,
),
),
);
},
),
),
);
}
await tester.pumpWidget(buildApp());
// RangeSlider does not have overlay when enabled and not hovered.
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Hover on the range slider but outside the thumb.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getTopLeft(find.byType(RangeSlider)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Hover on the thumb.
await gesture.moveTo(tester.getCenter(find.byType(RangeSlider)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: hoverColor),
);
// Hover on the slider but outside the thumb.
await gesture.moveTo(tester.getBottomRight(find.byType(RangeSlider)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: hoverColor)),
);
// Reset range slider values.
values = const RangeValues(50, 70);
// RangeSlider does not have overlay when enabled and not dragged.
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: draggedColor)),
);
// Start dragging.
final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider)));
await tester.pump(kPressTimeout);
// Less than configured touch slop, more than default touch slop.
await drag.moveBy(const Offset(19.0, 0));
await tester.pump();
// RangeSlider has overlay when enabled and dragged.
expect(
Material.of(tester.element(find.byType(RangeSlider))),
paints..circle(color: draggedColor),
);
// Stop dragging.
await drag.up();
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(RangeSlider))),
isNot(paints..circle(color: draggedColor)),
);
});
}