| // 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/services.dart'; |
| import 'package:flutter/src/physics/utils.dart' show nearEqual; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../widgets/semantics_tester.dart'; |
| |
| void main() { |
| // Regression test for https://github.com/flutter/flutter/issues/105833 |
| testWidgets('Drag gesture uses provided gesture settings', (WidgetTester tester) async { |
| var values = const RangeValues(0.1, 0.5); |
| var 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); |
| |
| var 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 { |
| var values = const RangeValues(0.3, 0.8); |
| |
| 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; |
| }); |
| }, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The closest thumb is selected when tapping between the thumbs outside the touch |
| // boundaries |
| expect(values, equals(const RangeValues(0.3, 0.8))); |
| // taps at 0.5 |
| await tester.tap(find.byType(RangeSlider)); |
| await tester.pump(); |
| expect(values, equals(const RangeValues(0.5, 0.8))); |
| |
| // 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.8)); |
| |
| // 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 { |
| var values = const RangeValues(0.3, 1.0); |
| |
| 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; |
| }); |
| }, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The closest thumb is selected when tapping between the thumbs outside the touch |
| // boundaries |
| expect(values, equals(const RangeValues(0.3, 1.0))); |
| // taps at 0.5 |
| await tester.tap(find.byType(RangeSlider)); |
| await tester.pump(); |
| expect(values, equals(const RangeValues(0.5, 1.0))); |
| |
| // 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.5); |
| 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 { |
| var values = const RangeValues(30, 80); |
| |
| 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; |
| }); |
| }, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The closest thumb is selected when tapping between the thumbs outside the touch |
| // boundaries |
| expect(values, equals(const RangeValues(30, 80))); |
| // taps at 0.5 |
| await tester.tap(find.byType(RangeSlider)); |
| await tester.pumpAndSettle(); |
| expect(values, equals(const RangeValues(50, 80))); |
| |
| // 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(80)); |
| |
| // 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 { |
| var values = const RangeValues(30, 80); |
| |
| 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; |
| }); |
| }, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // The closest thumb is selected when tapping between the thumbs outside the touch |
| // boundaries |
| expect(values, equals(const RangeValues(30, 80))); |
| // taps at 0.5 |
| await tester.tap(find.byType(RangeSlider)); |
| await tester.pumpAndSettle(); |
| expect(values, equals(const RangeValues(50, 80))); |
| |
| // 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(50)); |
| 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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)); |
| 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 end thumb can be dragged apart (continuous LTR)', |
| (WidgetTester tester) async { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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 { |
| var 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, |
| }) { |
| var 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) |
| ..rrect(color: sliderTheme.inactiveTrackColor) |
| ..rrect(color: sliderTheme.activeTrackColor), |
| ); |
| 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 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) |
| ..rrect(color: sliderTheme.inactiveTrackColor) |
| ..rrect(color: activeColor), |
| ); |
| 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 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) |
| ..rrect(color: inactiveColor) |
| ..rrect(color: sliderTheme.activeTrackColor), |
| ); |
| 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 activeColor = Color(0xcafefeed); |
| const 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) |
| ..rrect(color: inactiveColor) |
| ..rrect(color: activeColor), |
| ); |
| 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) |
| ..rrect(color: sliderTheme.inactiveTrackColor) |
| ..rrect(color: sliderTheme.activeTrackColor), |
| ); |
| 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 activeColor = Color(0xcafefeed); |
| const 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) |
| ..rrect(color: inactiveColor) |
| ..rrect(color: activeColor), |
| ); |
| 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) |
| ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
| ..rrect(color: sliderTheme.disabledActiveTrackColor), |
| ); |
| 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 activeColor = Color(0xcafefeed); |
| const 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) |
| ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
| ..rrect(color: sliderTheme.disabledActiveTrackColor), |
| ); |
| 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; |
| var 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 { |
| var values = const RangeValues(0.5, 0.75); |
| const fillColor = Color(0xf55f5f5f); |
| |
| Widget buildApp({ |
| Color? activeColor, |
| Color? inactiveColor, |
| int? divisions, |
| bool enabled = true, |
| }) { |
| void onChanged(RangeValues newValues) { |
| values = newValues; |
| } |
| |
| return MaterialApp( |
| theme: ThemeData(useMaterial3: false), |
| 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: 5)); |
| |
| 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 { |
| var values = const RangeValues(0.3, 0.7); |
| |
| final 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 { |
| var values = const RangeValues(0.3, 0.7); |
| |
| final 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 { |
| var values = const RangeValues(0.3, 0.7); |
| |
| final 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(textScaler: TextScaler.linear(2)), |
| 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 { |
| var values = const RangeValues(0.3, 0.7); |
| |
| final 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(), |
| 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( |
| children: <Matcher>[ |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: true, |
| hasEnabledState: true, |
| hasIncreaseAction: true, |
| hasDecreaseAction: true, |
| value: '10%', |
| increasedValue: '10%', |
| decreasedValue: '5%', |
| label: '', |
| ), |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: 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(), |
| 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( |
| children: <Matcher>[ |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: 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, |
| isFocusable: 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 rects = <Rect>[]; |
| semanticsNode.visitChildren((SemanticsNode node) { |
| node.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; |
| }); |
| 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(), |
| 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( |
| children: <Matcher>[ |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: true, |
| hasEnabledState: true, |
| hasIncreaseAction: true, |
| hasDecreaseAction: true, |
| value: '10%', |
| increasedValue: '15%', |
| decreasedValue: '5%', |
| ), |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: 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 rects = <Rect>[]; |
| semanticsNode.visitChildren((SemanticsNode node) { |
| node.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; |
| }); |
| 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 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: ${const Color(0xff2196f3)})', |
| 'inactiveColor: MaterialColor(primary value: ${const 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), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderObject renderObject = tester.allRenderObjects |
| .where( |
| (RenderObject renderObject) => |
| renderObject.runtimeType.toString() == '_RenderRangeSlider', |
| ) |
| .first; |
| |
| 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), |
| ), |
| ) |
| // 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), |
| ), |
| ) |
| // active track RRect |
| ..rrect(rrect: RRect.fromLTRBR(-14.0, 2.0, 2.0, 8.0, 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), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderObject renderObject = tester.allRenderObjects |
| .where( |
| (RenderObject renderObject) => |
| renderObject.runtimeType.toString() == '_RenderRangeSlider', |
| ) |
| .first; |
| |
| //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)); |
| |
| final RenderObject renderObject = tester.allRenderObjects |
| .where( |
| (RenderObject renderObject) => |
| renderObject.runtimeType.toString() == '_RenderRangeSlider', |
| ) |
| .first; |
| |
| // 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 RRect activeTrackRRect; |
| expect( |
| renderObject, |
| paints |
| ..rrect() |
| ..rrect() |
| ..something((Symbol method, List<dynamic> arguments) { |
| if (method != #drawRRect) { |
| return false; |
| } |
| activeTrackRRect = arguments[0] as RRect; |
| return true; |
| }), |
| ); |
| |
| const padding = 4.0; |
| // 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 - padding) is the slider's width. |
| // Where the padding value equals to the track height. |
| expect( |
| nearEqual(activeTrackRRect.left, (800.0 - 24.0 - 24.0 - padding) * (5 / 15) + 24.0, 0.01), |
| true, |
| ); |
| expect( |
| nearEqual( |
| activeTrackRRect.right, |
| (800.0 - 24.0 - 24.0 - padding) * (8 / 15) + 24.0 + padding, |
| 0.01, |
| ), |
| true, |
| ); |
| }); |
| |
| testWidgets('RangeSlider changes mouse cursor when hovered', (WidgetTester tester) async { |
| const 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 WidgetStateMouseCursor resolves correctly', (WidgetTester tester) async { |
| var values = const RangeValues(20, 75); |
| const MouseCursor systemDefaultCursor = SystemMouseCursors.basic; |
| const MouseCursor disabledCursor = SystemMouseCursors.forbidden; |
| const MouseCursor draggedCursor = SystemMouseCursors.move; |
| const MouseCursor hoveredCursor = SystemMouseCursors.grab; |
| |
| Widget buildFrame({required bool enabled}) { |
| return MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: Column( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: [ |
| Center( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return RangeSlider( |
| mouseCursor: WidgetStateProperty.resolveWith<MouseCursor?>(( |
| Set<WidgetState> states, |
| ) { |
| if (states.contains(WidgetState.disabled)) { |
| return disabledCursor; |
| } |
| if (states.contains(WidgetState.dragged)) { |
| return draggedCursor; |
| } |
| if (states.contains(WidgetState.hovered)) { |
| return hoveredCursor; |
| } |
| return SystemMouseCursors.click; |
| }), |
| values: values, |
| max: 100.0, |
| onChanged: enabled |
| ? (RangeValues newValues) { |
| setState(() { |
| values = newValues; |
| }); |
| } |
| : null, |
| ); |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| final TestGesture gesture = await tester.createGesture( |
| kind: PointerDeviceKind.mouse, |
| pointer: 1, |
| ); |
| addTearDown(gesture.removePointer); |
| |
| // System default. |
| await gesture.addPointer(location: Offset.zero); |
| await tester.pumpWidget(buildFrame(enabled: false)); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), systemDefaultCursor); |
| |
| // Disabled. |
| await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); |
| await tester.pump(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor); |
| |
| // Hovered. |
| await tester.pumpWidget(buildFrame(enabled: true)); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor); |
| |
| // Dragged. |
| await gesture.down(tester.getCenter(find.byType(RangeSlider))); |
| await gesture.moveBy(const Offset(20.0, 0.0)); |
| await tester.pump(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), draggedCursor); |
| |
| // Hovered. |
| await gesture.up(); |
| await tester.pump(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor); |
| |
| // System default. |
| await gesture.moveTo(Offset.zero); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pump(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), systemDefaultCursor); |
| }); |
| |
| testWidgets('RangeSlider can be hovered and has correct hover color', ( |
| WidgetTester tester, |
| ) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| var values = const RangeValues(50, 70); |
| final 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 can be focused using keyboard focus', (WidgetTester tester) async { |
| var values = const RangeValues(20, 80); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Center( |
| child: RangeSlider( |
| values: values, |
| max: 100, |
| onChanged: (RangeValues newValues) { |
| setState(() { |
| values = newValues; |
| }); |
| }, |
| onChangeStart: (RangeValues newValues) {}, |
| onChangeEnd: (RangeValues newValues) {}, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| // Focus on the start thumb |
| final Finder rangeSliderFinder = find.byType(RangeSlider); |
| expect(rangeSliderFinder, findsOneWidget); |
| final startFocusNode = |
| (tester.firstState(find.byType(RangeSlider)) as dynamic).startFocusNode as FocusNode; |
| final endFocusNode = |
| (tester.firstState(find.byType(RangeSlider)) as dynamic).endFocusNode as FocusNode; |
| |
| startFocusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| expect(FocusManager.instance.primaryFocus, startFocusNode); |
| |
| // Tab to focus on the end thumb |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pumpAndSettle(); |
| expect(FocusManager.instance.primaryFocus, endFocusNode); |
| }); |
| |
| testWidgets('Keyboard focus also changes semantics focus', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Theme( |
| data: ThemeData(), |
| 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 startFocusNode = |
| (tester.firstState(find.byType(RangeSlider)) as dynamic).startFocusNode as FocusNode; |
| final endFocusNode = |
| (tester.firstState(find.byType(RangeSlider)) as dynamic).endFocusNode as FocusNode; |
| // Focus on the start thumb |
| startFocusNode.requestFocus(); |
| await tester.pumpAndSettle(); |
| expect(FocusManager.instance.primaryFocus, startFocusNode); |
| |
| final SemanticsNode semanticsNode = tester.getSemantics(find.byType(RangeSlider)); |
| expect( |
| semanticsNode, |
| matchesSemantics( |
| scopesRoute: true, |
| children: <Matcher>[ |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: true, |
| isFocused: 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, |
| isFocusable: 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), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ); |
| |
| // Tab to focus on the end thumb |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pumpAndSettle(); |
| expect(FocusManager.instance.primaryFocus, endFocusNode); |
| |
| expect( |
| semanticsNode, |
| matchesSemantics( |
| scopesRoute: true, |
| children: <Matcher>[ |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| isEnabled: true, |
| isSlider: true, |
| isFocusable: 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, |
| isFocusable: true, |
| isFocused: 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), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ); |
| }); |
| |
| testWidgets('RangeSlider is draggable and has correct dragged color', ( |
| WidgetTester tester, |
| ) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| var values = const RangeValues(50, 70); |
| final 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; |
| var values = const RangeValues(50, 70); |
| const hoverColor = Color(0xffff0000); |
| const 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: WidgetStateProperty.resolveWith<Color?>(( |
| Set<WidgetState> states, |
| ) { |
| if (states.contains(WidgetState.hovered)) { |
| return hoverColor; |
| } |
| if (states.contains(WidgetState.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)), |
| ); |
| }); |
| |
| testWidgets('RangeSlider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/128433 |
| |
| var startFired = 0; |
| var endFired = 0; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: Center( |
| child: GestureDetector( |
| onHorizontalDragUpdate: (_) {}, |
| child: RangeSlider( |
| values: const RangeValues(40, 80), |
| max: 100, |
| onChanged: (RangeValues newValue) {}, |
| onChangeStart: (RangeValues value) { |
| startFired += 1; |
| }, |
| onChangeEnd: (RangeValues value) { |
| endFired += 1; |
| }, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.timedDragFrom( |
| tester.getTopLeft(find.byType(RangeSlider)), |
| const Offset(100.0, 0.0), |
| const Duration(milliseconds: 500), |
| ); |
| |
| expect(startFired, equals(1)); |
| expect(endFired, equals(1)); |
| }); |
| |
| testWidgets('RangeSlider in a ListView does not throw an exception', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/126648 |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: ListView( |
| children: <Widget>[ |
| const SizedBox(height: 600, child: Placeholder()), |
| RangeSlider( |
| values: const RangeValues(40, 80), |
| max: 100, |
| onChanged: (RangeValues newValue) {}, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // No exception should be thrown. |
| expect(tester.takeException(), null); |
| }); |
| |
| // This is a regression test for https://github.com/flutter/flutter/issues/141953. |
| testWidgets('Semantic nodes do not throw an error after clearSemantics', ( |
| WidgetTester tester, |
| ) async { |
| var semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: MaterialApp( |
| home: Scaffold( |
| body: RangeSlider( |
| values: const RangeValues(40, 80), |
| max: 100, |
| onChanged: (RangeValues newValue) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Dispose the semantics to trigger clearSemantics. |
| semantics.dispose(); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.takeException(), isNull); |
| |
| // Initialize the semantics again. |
| semantics = SemanticsTester(tester); |
| await tester.pumpAndSettle(); |
| |
| expect(tester.takeException(), isNull); |
| |
| semantics.dispose(); |
| }, semanticsEnabled: false); |
| |
| testWidgets('Value indicator appears when it should', (WidgetTester tester) async { |
| final baseTheme = ThemeData(platform: TargetPlatform.android, primarySwatch: Colors.blue); |
| SliderThemeData theme = baseTheme.sliderTheme.copyWith(valueIndicatorColor: Colors.red); |
| var value = const RangeValues(1, 5); |
| Widget buildApp({required SliderThemeData sliderTheme, int? divisions, bool enabled = true}) { |
| final ValueChanged<RangeValues>? onChanged = enabled ? (RangeValues d) => value = d : null; |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: Theme( |
| data: baseTheme, |
| child: SliderTheme( |
| data: sliderTheme, |
| child: RangeSlider( |
| values: value, |
| max: 10, |
| labels: RangeLabels(value.start.toString(), value.end.toString()), |
| divisions: divisions, |
| onChanged: onChanged, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> expectValueIndicator({ |
| required bool isVisible, |
| required SliderThemeData theme, |
| int? divisions, |
| bool enabled = true, |
| bool dragged = true, |
| }) async { |
| // Discrete enabled widget. |
| await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled)); |
| final Offset center = tester.getCenter(find.byType(RangeSlider)); |
| TestGesture? gesture; |
| if (dragged) { |
| gesture = await tester.startGesture(center); |
| } |
| // Wait for value indicator animation to finish. |
| await tester.pumpAndSettle(); |
| |
| // _RenderValueIndicator is the last render object in the tree. |
| final RenderObject valueIndicatorBox = tester.allRenderObjects.last; |
| expect( |
| valueIndicatorBox, |
| isVisible |
| ? (paints |
| ..path(color: theme.valueIndicatorColor) |
| ..paragraph()) |
| : isNot( |
| paints |
| ..path(color: theme.valueIndicatorColor) |
| ..paragraph(), |
| ), |
| ); |
| if (dragged) { |
| await gesture!.up(); |
| } |
| } |
| |
| // Default (showValueIndicator set to onlyForDiscrete). |
| await expectValueIndicator(isVisible: true, theme: theme, divisions: 10); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, dragged: false); |
| await expectValueIndicator( |
| isVisible: false, |
| theme: theme, |
| divisions: 3, |
| enabled: false, |
| dragged: false, |
| ); |
| await expectValueIndicator(isVisible: false, theme: theme, dragged: false); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false, dragged: false); |
| |
| // With showValueIndicator set to onlyForContinuous. |
| theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onlyForContinuous); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, enabled: false); |
| await expectValueIndicator(isVisible: true, theme: theme); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, dragged: false); |
| await expectValueIndicator( |
| isVisible: false, |
| theme: theme, |
| divisions: 3, |
| enabled: false, |
| dragged: false, |
| ); |
| await expectValueIndicator(isVisible: false, theme: theme, dragged: false); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false, dragged: false); |
| |
| // discrete enabled widget with showValueIndicator set to onDrag. |
| theme = theme.copyWith(showValueIndicator: ShowValueIndicator.onDrag); |
| await expectValueIndicator(isVisible: true, theme: theme, divisions: 10); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, enabled: false); |
| await expectValueIndicator(isVisible: true, theme: theme); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, dragged: false); |
| await expectValueIndicator( |
| isVisible: false, |
| theme: theme, |
| divisions: 3, |
| enabled: false, |
| dragged: false, |
| ); |
| await expectValueIndicator(isVisible: false, theme: theme, dragged: false); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false, dragged: false); |
| |
| // discrete enabled widget with showValueIndicator set to never. |
| theme = theme.copyWith(showValueIndicator: ShowValueIndicator.never); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false); |
| await expectValueIndicator(isVisible: false, theme: theme, divisions: 10, dragged: false); |
| await expectValueIndicator( |
| isVisible: false, |
| theme: theme, |
| divisions: 3, |
| enabled: false, |
| dragged: false, |
| ); |
| await expectValueIndicator(isVisible: false, theme: theme, dragged: false); |
| await expectValueIndicator(isVisible: false, theme: theme, enabled: false, dragged: false); |
| |
| // discrete enabled widget with showValueIndicator set to alwaysVisible. |
| theme = theme.copyWith(showValueIndicator: ShowValueIndicator.alwaysVisible); |
| await expectValueIndicator(isVisible: true, theme: theme, divisions: 3); |
| await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, enabled: false); |
| await expectValueIndicator(isVisible: true, theme: theme); |
| await expectValueIndicator(isVisible: true, theme: theme, enabled: false); |
| await expectValueIndicator(isVisible: true, theme: theme, divisions: 3, dragged: false); |
| await expectValueIndicator( |
| isVisible: true, |
| theme: theme, |
| divisions: 3, |
| enabled: false, |
| dragged: false, |
| ); |
| await expectValueIndicator(isVisible: true, theme: theme, dragged: false); |
| await expectValueIndicator(isVisible: true, theme: theme, enabled: false, dragged: false); |
| }); |
| |
| testWidgets('RangeSlider overlay appears correctly for specific thumb interactions', ( |
| WidgetTester tester, |
| ) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| var values = const RangeValues(50, 70); |
| const hoverColor = Color(0xffff0000); |
| const dragColor = Color(0xff0000ff); |
| |
| Widget buildApp() { |
| 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: WidgetStateProperty.resolveWith<Color?>(( |
| Set<WidgetState> states, |
| ) { |
| if (states.contains(WidgetState.hovered)) { |
| return hoverColor; |
| } |
| if (states.contains(WidgetState.dragged)) { |
| return dragColor; |
| } |
| |
| return null; |
| }), |
| onChanged: (RangeValues newValues) { |
| setState(() { |
| values = newValues; |
| }); |
| }, |
| onChangeStart: (RangeValues newValues) {}, |
| onChangeEnd: (RangeValues newValues) {}, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildApp()); |
| await tester.pumpAndSettle(); |
| |
| // Initial state - no overlay. |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| isNot(paints..circle(color: dragColor)), |
| ); |
| |
| // Drag start thumb to left. |
| final Offset topThumbLocation = tester.getCenter(find.byType(RangeSlider)); |
| final TestGesture dragStartThumb = await tester.startGesture(topThumbLocation); |
| await tester.pump(kPressTimeout); |
| await dragStartThumb.moveBy(const Offset(-20.0, 0)); |
| await tester.pumpAndSettle(); |
| |
| // Verify overlay is visible and shadow is visible on single thumb. |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| paints |
| ..circle(color: dragColor) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 12.0), |
| ); |
| |
| // Move back and release. |
| await dragStartThumb.moveBy(const Offset(20.0, 0)); |
| await dragStartThumb.up(); |
| await tester.pumpAndSettle(); |
| |
| // Verify overlay and shadow disappears |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| isNot( |
| paints |
| ..circle(color: dragColor) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0), |
| ), |
| ); |
| |
| // Drag end thumb and return to original position. |
| final Offset bottomThumbLocation = tester |
| .getCenter(find.byType(RangeSlider)) |
| .translate(220.0, 0.0); |
| final TestGesture dragEndThumb = await tester.startGesture(bottomThumbLocation); |
| await tester.pump(kPressTimeout); |
| await dragEndThumb.moveBy(const Offset(20.0, 0)); |
| await tester.pump(kPressTimeout); |
| await dragEndThumb.moveBy(const Offset(-20.0, 0)); |
| await dragEndThumb.up(); |
| await tester.pumpAndSettle(); |
| |
| // Verify overlay disappears. |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| isNot(paints..circle(color: dragColor)), |
| ); |
| |
| // Hover on start thumb. |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(topThumbLocation); |
| await tester.pumpAndSettle(); |
| |
| // Verify overlay appears only for start thumb and no shadow is visible. |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| paints |
| ..circle(color: hoverColor) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
| ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0), |
| ); |
| |
| final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider)); |
| // 2 thumbs and 1 overlay. |
| expect(renderObject, paintsExactlyCountTimes(#drawCircle, 3)); |
| |
| // Move away from thumb |
| await gesture.moveTo(tester.getTopRight(find.byType(RangeSlider))); |
| await tester.pumpAndSettle(); |
| |
| // Verify overlay disappears |
| expect( |
| Material.of(tester.element(find.byType(RangeSlider))), |
| isNot(paints..circle(color: hoverColor)), |
| ); |
| }); |
| |
| testWidgets('RangeSlider.padding can override the default RangeSlider padding', ( |
| WidgetTester tester, |
| ) async { |
| Widget buildRangeSlider({EdgeInsetsGeometry? padding}) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: IntrinsicHeight( |
| child: RangeSlider( |
| padding: padding, |
| values: const RangeValues(0, 1.0), |
| onChanged: (RangeValues values) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| RenderBox sliderRenderBox() { |
| return tester.allRenderObjects.firstWhere( |
| (RenderObject object) => object.runtimeType.toString() == '_RenderRangeSlider', |
| ) |
| as RenderBox; |
| } |
| |
| // Test RangeSlider height and tracks spacing with zero padding. |
| await tester.pumpWidget(buildRangeSlider(padding: EdgeInsets.zero)); |
| await tester.pumpAndSettle(); |
| |
| // The height equals to the default thumb height. |
| expect(sliderRenderBox().size, const Size(800, 20)); |
| expect( |
| find.byType(RangeSlider), |
| paints |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 10.0, |
| 8.0, |
| 10.0, |
| 12.0, |
| topLeft: const Radius.circular(2.0), |
| bottomLeft: const Radius.circular(2.0), |
| ), |
| ) |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 790.0, |
| 8.0, |
| 790.0, |
| 12.0, |
| topRight: const Radius.circular(2.0), |
| bottomRight: const Radius.circular(2.0), |
| ), |
| ) |
| // Active track. |
| ..rrect(rrect: RRect.fromLTRBR(8.0, 7.0, 792.0, 13.0, const Radius.circular(2.0))), |
| ); |
| |
| // Test RangeSlider height and tracks spacing with directional padding. |
| const double startPadding = 100; |
| const double endPadding = 20; |
| await tester.pumpWidget( |
| buildRangeSlider( |
| padding: const EdgeInsetsDirectional.only(start: startPadding, end: endPadding), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect(sliderRenderBox().size, const Size(800 - startPadding - endPadding, 20)); |
| expect( |
| find.byType(RangeSlider), |
| paints |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 10.0, |
| 8.0, |
| 10.0, |
| 12.0, |
| topLeft: const Radius.circular(2.0), |
| bottomLeft: const Radius.circular(2.0), |
| ), |
| ) |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 670.0, |
| 8.0, |
| 670.0, |
| 12.0, |
| topRight: const Radius.circular(2.0), |
| bottomRight: const Radius.circular(2.0), |
| ), |
| ) |
| // Active track. |
| ..rrect(rrect: RRect.fromLTRBR(8.0, 7.0, 672.0, 13.0, const Radius.circular(2.0))), |
| ); |
| |
| // Test RangeSlider height and tracks spacing with top and bottom padding. |
| const double topPadding = 100; |
| const double bottomPadding = 20; |
| const double trackHeight = 20; |
| await tester.pumpWidget( |
| buildRangeSlider( |
| padding: const EdgeInsetsDirectional.only(top: topPadding, bottom: bottomPadding), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| tester.getSize(find.byType(RangeSlider)), |
| const Size(800, topPadding + trackHeight + bottomPadding), |
| ); |
| expect(sliderRenderBox().size, const Size(800, 20)); |
| expect( |
| find.byType(RangeSlider), |
| paints |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 10.0, |
| 8.0, |
| 10.0, |
| 12.0, |
| topLeft: const Radius.circular(2.0), |
| bottomLeft: const Radius.circular(2.0), |
| ), |
| ) |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 790.0, |
| 8.0, |
| 790.0, |
| 12.0, |
| topRight: const Radius.circular(2.0), |
| bottomRight: const Radius.circular(2.0), |
| ), |
| ) |
| // Active track. |
| ..rrect(rrect: RRect.fromLTRBR(8.0, 7.0, 792.0, 13.0, const Radius.circular(2.0))), |
| ); |
| }); |
| |
| // Regression test for hhttps://github.com/flutter/flutter/issues/161805 |
| testWidgets('Discrete RangeSlider does not apply thumb padding in a non-rounded track shape', ( |
| WidgetTester tester, |
| ) async { |
| // The default track left and right padding. |
| const sliderPadding = 24.0; |
| final theme = ThemeData( |
| sliderTheme: const SliderThemeData( |
| // Thumb padding is applied based on the track height. |
| trackHeight: 100, |
| rangeTrackShape: RectangularRangeSliderTrackShape(), |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: Material( |
| child: SizedBox( |
| width: 300, |
| child: RangeSlider( |
| values: const RangeValues(0, 100), |
| max: 100, |
| divisions: 100, |
| onChanged: (RangeValues value) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final MaterialInkController material = Material.of(tester.element(find.byType(RangeSlider))); |
| |
| expect( |
| material, |
| paints |
| // Start thumb. |
| ..circle(x: sliderPadding, y: 300.0, color: theme.colorScheme.primary) |
| // End thumb. |
| ..circle(x: 800.0 - sliderPadding, y: 300.0, color: theme.colorScheme.primary), |
| ); |
| }); |
| |
| testWidgets('Default RangeSlider when year2023 is false', (WidgetTester tester) async { |
| final theme = ThemeData(); |
| final ColorScheme colorScheme = theme.colorScheme; |
| final Color activeTrackColor = colorScheme.primary; |
| final Color inactiveTrackColor = colorScheme.secondaryContainer; |
| final Color disabledActiveTrackColor = colorScheme.onSurface.withOpacity(0.38); |
| final Color disabledInactiveTrackColor = colorScheme.onSurface.withOpacity(0.12); |
| final Color activeTickMarkColor = colorScheme.onPrimary; |
| final Color inactiveTickMarkColor = colorScheme.onSecondaryContainer; |
| final Color disabledActiveTickMarkColor = colorScheme.onInverseSurface; |
| final Color disabledInactiveTickMarkColor = colorScheme.onSurface; |
| final Color thumbColor = colorScheme.primary; |
| final Color disabledThumbColor = colorScheme.onSurface.withOpacity(0.38); |
| final Color valueIndicatorColor = colorScheme.inverseSurface; |
| var values = const RangeValues(25.0, 75.0); |
| Widget buildApp({int? divisions, bool enabled = true}) { |
| final ValueChanged<RangeValues>? onChanged = !enabled |
| ? null |
| : (RangeValues newValues) { |
| values = newValues; |
| }; |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: Theme( |
| data: theme, |
| child: RangeSlider( |
| year2023: false, |
| values: values, |
| max: 100, |
| labels: RangeLabels(values.start.round().toString(), values.end.round().toString()), |
| divisions: divisions, |
| onChanged: onChanged, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildApp()); |
| |
| final MaterialInkController material = Material.of(tester.element(find.byType(RangeSlider))); |
| |
| // Test default track shape. |
| const trackOuterCornerRadius = Radius.circular(8.0); |
| const trackInnerCornerRadius = Radius.circular(2.0); |
| expect( |
| material, |
| paints |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 24.0, |
| 292.0, |
| 206.0, |
| 308.0, |
| topLeft: trackOuterCornerRadius, |
| topRight: trackInnerCornerRadius, |
| bottomRight: trackInnerCornerRadius, |
| bottomLeft: trackOuterCornerRadius, |
| ), |
| color: inactiveTrackColor, |
| ) |
| // Inactive track. |
| ..rrect( |
| rrect: RRect.fromLTRBAndCorners( |
| 594.0, |
| 292.0, |
| 776.0, |
| 308.0, |
| topLeft: trackInnerCornerRadius, |
| topRight: trackOuterCornerRadius, |
| bottomRight: trackOuterCornerRadius, |
| bottomLeft: trackInnerCornerRadius, |
| ), |
| color: inactiveTrackColor, |
| ) |
| // Active track. |
| ..rrect( |
| rrect: RRect.fromLTRBR(218.0, 292.0, 582.0, 308.0, trackInnerCornerRadius), |
| color: activeTrackColor, |
| ), |
| ); |
| |
| // Test default colors for enabled slider. |
| expect( |
| material, |
| paints |
| ..circle() |
| ..circle() |
| ..rrect(color: thumbColor) |
| ..rrect(color: thumbColor), |
| ); |
| expect( |
| material, |
| isNot( |
| paints |
| ..circle() |
| ..circle() |
| ..rrect(color: disabledThumbColor) |
| ..rrect(color: disabledThumbColor), |
| ), |
| ); |
| expect(material, isNot(paints..rrect(color: disabledActiveTrackColor))); |
| expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor))); |
| |
| // Test defaults colors for discrete slider. |
| await tester.pumpWidget(buildApp(divisions: 4)); |
| expect( |
| material, |
| paints |
| ..rrect(color: inactiveTrackColor) |
| ..rrect(color: inactiveTrackColor) |
| ..rrect(color: activeTrackColor) |
| ..circle(color: inactiveTickMarkColor) |
| ..circle(color: activeTickMarkColor) |
| ..circle(color: inactiveTickMarkColor), |
| ); |
| expect(material, isNot(paints..rrect(color: disabledThumbColor))); |
| expect(material, isNot(paints..rrect(color: disabledActiveTrackColor))); |
| expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor))); |
| |
| // Test defaults colors for disabled slider. |
| await tester.pumpWidget(buildApp(enabled: false)); |
| await tester.pumpAndSettle(); |
| expect( |
| material, |
| paints |
| ..rrect(color: disabledInactiveTrackColor) |
| ..rrect(color: disabledInactiveTrackColor) |
| ..rrect(color: disabledActiveTrackColor) |
| ..rrect(color: disabledThumbColor) |
| ..rrect(color: disabledThumbColor), |
| ); |
| expect( |
| material, |
| isNot( |
| paints |
| ..rrect(color: thumbColor) |
| ..rrect(color: thumbColor), |
| ), |
| ); |
| expect(material, isNot(paints..rrect(color: activeTrackColor))); |
| expect(material, isNot(paints..rrect(color: inactiveTrackColor))); |
| |
| // Test defaults colors for disabled discrete slider. |
| await tester.pumpWidget(buildApp(divisions: 4, enabled: false)); |
| expect( |
| material, |
| paints |
| ..rrect(color: disabledInactiveTrackColor) |
| ..rrect(color: disabledInactiveTrackColor) |
| ..rrect(color: disabledActiveTrackColor) |
| ..circle(color: disabledInactiveTickMarkColor) |
| ..circle(color: disabledActiveTickMarkColor) |
| ..circle(color: disabledInactiveTickMarkColor) |
| ..rrect(color: disabledThumbColor) |
| ..rrect(color: disabledThumbColor), |
| ); |
| expect( |
| material, |
| isNot( |
| paints |
| ..rrect(color: thumbColor) |
| ..rrect(color: thumbColor), |
| ), |
| ); |
| expect(material, isNot(paints..rrect(color: activeTrackColor))); |
| expect(material, isNot(paints..rrect(color: inactiveTrackColor))); |
| |
| await tester.pumpWidget(buildApp(divisions: 4)); |
| await tester.pumpAndSettle(); |
| |
| final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)); |
| final TestGesture gesture = await tester.startGesture(topLeft); |
| // Wait for value indicator animation to finish. |
| await tester.pumpAndSettle(); |
| |
| final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
| expect( |
| valueIndicatorBox, |
| paints |
| ..scale() |
| ..rrect(color: valueIndicatorColor), |
| ); |
| await gesture.up(); |
| }); |
| |
| testWidgets('RangeSlider value indicator text when year2023 is false', ( |
| WidgetTester tester, |
| ) async { |
| const values = RangeValues(25.0, 75.0); |
| final log = <InlineSpan>[]; |
| final loggingValueIndicatorShape = LoggingRangeSliderValueIndicatorShape(log); |
| final theme = ThemeData( |
| sliderTheme: SliderThemeData(rangeValueIndicatorShape: loggingValueIndicatorShape), |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: RangeSlider( |
| year2023: false, |
| values: values, |
| max: 100, |
| labels: RangeLabels(values.start.round().toString(), values.end.round().toString()), |
| divisions: 4, |
| onChanged: (RangeValues value) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)); |
| final TestGesture gesture = await tester.startGesture(topLeft); |
| await tester.pumpAndSettle(); |
| |
| expect(log.last.toPlainText(), '25'); |
| expect(log.last.style!.fontSize, 14.0); |
| expect(log.last.style!.color, theme.colorScheme.onInverseSurface); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('RangeSlider supports DropRangeSliderValueIndicatorShape', ( |
| WidgetTester tester, |
| ) async { |
| const values = RangeValues(25.0, 75.0); |
| const valueIndicatorColor = Color(0XFFFF0000); |
| final theme = ThemeData( |
| sliderTheme: const SliderThemeData( |
| rangeValueIndicatorShape: DropRangeSliderValueIndicatorShape(), |
| valueIndicatorColor: valueIndicatorColor, |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: RangeSlider( |
| year2023: false, |
| values: values, |
| max: 100, |
| labels: RangeLabels(values.start.round().toString(), values.end.round().toString()), |
| divisions: 4, |
| onChanged: (RangeValues value) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)); |
| final TestGesture gesture = await tester.startGesture(topLeft); |
| await tester.pumpAndSettle(); |
| |
| final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
| expect(valueIndicatorBox, paints..path(color: valueIndicatorColor)); |
| |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('Value indicator appears on tap', (WidgetTester tester) async { |
| final ThemeData theme = buildTheme(); |
| final SliderThemeData sliderTheme = theme.sliderTheme; |
| const discreteValues = RangeValues(20, 40); |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: theme, |
| home: Material( |
| child: RangeSlider( |
| labels: RangeLabels( |
| discreteValues.start.round().toString(), |
| discreteValues.end.round().toString(), |
| ), |
| values: discreteValues, |
| divisions: 5, |
| max: 100, |
| onChanged: (RangeValues values) {}, |
| ), |
| ), |
| ), |
| ); |
| await tester.tap(find.byType(RangeSlider)); |
| await tester.pumpAndSettle(); |
| final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
| expect( |
| valueIndicatorBox, |
| paints |
| ..path(color: Colors.black) // shadow |
| ..path(color: Colors.black) // shadow |
| ..path(color: sliderTheme.valueIndicatorColor) |
| ..paragraph(), |
| ); |
| }); |
| |
| testWidgets('RangeSlider does not crash at zero area', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: Center( |
| child: SizedBox.shrink( |
| child: RangeSlider(values: const RangeValues(0, 1), onChanged: (_) {}), |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(RangeSlider)), Size.zero); |
| }); |
| } |
| |
| // A value indicator shape to log labelPainter text. |
| class LoggingRangeSliderValueIndicatorShape extends RangeSliderValueIndicatorShape { |
| LoggingRangeSliderValueIndicatorShape(this.logLabel); |
| |
| final List<InlineSpan> logLabel; |
| |
| @override |
| Size getPreferredSize( |
| bool isEnabled, |
| bool isDiscrete, { |
| required TextPainter labelPainter, |
| required double textScaleFactor, |
| }) { |
| return const Size(10.0, 10.0); |
| } |
| |
| @override |
| void paint( |
| PaintingContext context, |
| Offset center, { |
| required Animation<double> activationAnimation, |
| required Animation<double> enableAnimation, |
| bool? isDiscrete, |
| bool? isOnTop, |
| required TextPainter labelPainter, |
| double? textScaleFactor, |
| Size? sizeWithOverflow, |
| required RenderBox parentBox, |
| required SliderThemeData sliderTheme, |
| TextDirection? textDirection, |
| double? value, |
| Thumb? thumb, |
| }) { |
| logLabel.add(labelPainter.text!); |
| } |
| } |