// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter/src/physics/utils.dart' show nearEqual; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; /// A [RoundedRectSliderTrackShape] that logs its paint. class LoggingRoundedRectSliderTrackShape extends RoundedRectSliderTrackShape { LoggingRoundedRectSliderTrackShape({this.secondaryOffsetLog}); final List? secondaryOffsetLog; @override Rect getPreferredRect({ required RenderBox parentBox, Offset offset = Offset.zero, required SliderThemeData sliderTheme, bool isEnabled = true, bool isDiscrete = false, }) { return super.getPreferredRect( parentBox: parentBox, offset: offset, sliderTheme: sliderTheme, isEnabled: isEnabled, isDiscrete: isDiscrete, ); } @override void paint( PaintingContext context, Offset offset, { required RenderBox parentBox, required SliderThemeData sliderTheme, required Animation enableAnimation, required TextDirection textDirection, required Offset thumbCenter, Offset? secondaryOffset, bool isDiscrete = false, bool isEnabled = false, double additionalActiveTrackHeight = 2, }) { secondaryOffsetLog?.add(secondaryOffset); super.paint( context, offset, parentBox: parentBox, sliderTheme: sliderTheme, enableAnimation: enableAnimation, textDirection: textDirection, thumbCenter: thumbCenter, secondaryOffset: secondaryOffset, isDiscrete: isDiscrete, isEnabled: isEnabled, additionalActiveTrackHeight: additionalActiveTrackHeight, ); } } // A thumb shape that also logs its repaint center. class LoggingThumbShape extends SliderComponentShape { LoggingThumbShape(this.log); final List log; @override Size getPreferredSize(bool isEnabled, bool isDiscrete) { return const Size(10.0, 10.0); } @override void paint( PaintingContext context, Offset thumbCenter, { required Animation activationAnimation, required Animation enableAnimation, required bool isDiscrete, required TextPainter labelPainter, required RenderBox parentBox, required SliderThemeData sliderTheme, required TextDirection textDirection, required double value, required double textScaleFactor, required Size sizeWithOverflow, }) { log.add(thumbCenter); final thumbPaint = Paint()..color = Colors.red; context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint); } } // A value indicator shape to log labelPainter text. class LoggingValueIndicatorShape extends SliderComponentShape { LoggingValueIndicatorShape(this.logLabel); final List logLabel; @override Size getPreferredSize(bool isEnabled, bool isDiscrete) { return const Size(10.0, 10.0); } @override void paint( PaintingContext context, Offset offset, { required Animation activationAnimation, required Animation enableAnimation, required bool isDiscrete, required TextPainter labelPainter, required RenderBox parentBox, required SliderThemeData sliderTheme, required TextDirection textDirection, required double value, required double textScaleFactor, required Size sizeWithOverflow, }) { logLabel.add(labelPainter.text!); } } class TallSliderTickMarkShape extends SliderTickMarkShape { @override Size getPreferredSize({required SliderThemeData sliderTheme, required bool isEnabled}) { return const Size(10.0, 200.0); } @override void paint( PaintingContext context, Offset offset, { required Offset thumbCenter, required RenderBox parentBox, required SliderThemeData sliderTheme, required Animation enableAnimation, required bool isEnabled, required TextDirection textDirection, }) { final paint = Paint()..color = Colors.red; context.canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, 10.0, 20.0), paint); } } class _StateDependentMouseCursor extends WidgetStateMouseCursor { const _StateDependentMouseCursor({ this.disabled = SystemMouseCursors.none, this.focused = SystemMouseCursors.none, this.dragged = SystemMouseCursors.none, this.hovered = SystemMouseCursors.none, this.regular = SystemMouseCursors.none, }); final MouseCursor disabled; final MouseCursor focused; final MouseCursor hovered; final MouseCursor dragged; final MouseCursor regular; @override MouseCursor resolve(Set states) { if (states.contains(WidgetState.disabled)) { return disabled; } if (states.contains(WidgetState.focused)) { return focused; } if (states.contains(WidgetState.dragged)) { return dragged; } if (states.contains(WidgetState.hovered)) { return hovered; } return regular; } @override String get debugDescription => '_StateDependentMouseCursor'; } void main() { testWidgets('The initial value should respect the discrete value', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.20; final log = []; final loggingThumb = LoggingThumbShape(log); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { final SliderThemeData sliderTheme = SliderTheme.of( context, ).copyWith(thumbShape: loggingThumb); return Material( child: Center( child: SliderTheme( data: sliderTheme, child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ); }, ), ), ), ); expect(value, equals(0.20)); expect(log.length, 1); expect(log[0], const Offset(213.0, 300.0)); }); testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; double? startValue; double? endValue; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeStart: (double value) { startValue = value; }, onChangeEnd: (double value) { endValue = value; }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); expect(startValue, equals(0.0)); expect(endValue, equals(0.5)); startValue = null; endValue = null; await tester.pump(); // No animation should start. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey)); final Offset target = topLeft + (bottomRight - topLeft) / 4.0; await tester.tapAt(target); expect(value, moreOrLessEquals(0.25, epsilon: 0.05)); expect(startValue, equals(0.5)); expect(endValue, moreOrLessEquals(0.25, epsilon: 0.05)); await tester.pump(); // No animation should start. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider can move when tapped (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); await tester.pump(); // No animation should start. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey)); final Offset target = topLeft + (bottomRight - topLeft) / 4.0; await tester.tapAt(target); expect(value, moreOrLessEquals(0.75, epsilon: 0.05)); await tester.pump(); // No animation should start. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets("Slider doesn't send duplicate change events if tapped on the same value", ( WidgetTester tester, ) async { final Key sliderKey = UniqueKey(); var value = 0.0; late double startValue; late double endValue; var updates = 0; var startValueUpdates = 0; var endValueUpdates = 0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { updates++; value = newValue; }); }, onChangeStart: (double value) { startValueUpdates++; startValue = value; }, onChangeEnd: (double value) { endValueUpdates++; endValue = value; }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); expect(startValue, equals(0.0)); expect(endValue, equals(0.5)); await tester.pump(); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); await tester.pump(); expect(updates, equals(1)); expect(startValueUpdates, equals(2)); expect(endValueUpdates, equals(2)); }); testWidgets('Value indicator shows for a bit after being tapped', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); await tester.pump(const Duration(milliseconds: 100)); // Starts with the position animation and value indicator expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); await tester.pump(const Duration(milliseconds: 100)); // Value indicator is longer than position. expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); await tester.pump(const Duration(milliseconds: 100)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); await tester.pump(const Duration(milliseconds: 100)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); await tester.pump(const Duration(milliseconds: 100)); // Shown for long enough, value indicator is animated closed. expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); await tester.pump(const Duration(milliseconds: 101)); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Discrete Slider repaints and animates when dragged', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; final log = []; final loggingThumb = LoggingThumbShape(log); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { final SliderThemeData sliderTheme = SliderTheme.of( context, ).copyWith(thumbShape: loggingThumb); return Material( child: Center( child: SliderTheme( data: sliderTheme, child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ); }, ), ), ), ); final expectedLog = [ const Offset(26.0, 300.0), const Offset(26.0, 300.0), const Offset(400.0, 300.0), ]; final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(value, equals(0.5)); expect(log.length, 3); expect(log, orderedEquals(expectedLog)); await gesture.moveBy(const Offset(-500.0, 0.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 5); expect(log.last.dx, moreOrLessEquals(386.6, epsilon: 0.1)); // With no more gesture or value changes, the thumb position should still // be redrawn in the animated position. await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 7); expect(log.last.dx, moreOrLessEquals(344.8, epsilon: 0.1)); // Final position. await tester.pump(const Duration(milliseconds: 80)); expectedLog.add(const Offset(26.0, 300.0)); expect(value, equals(0.0)); expect(log.length, 8); expect(log.last.dx, moreOrLessEquals(26.0, epsilon: 0.1)); await gesture.up(); }); testWidgets("Slider doesn't send duplicate change events if tapped on the same value", ( WidgetTester tester, ) async { final Key sliderKey = UniqueKey(); var value = 0.0; var updates = 0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { updates++; value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); await tester.pump(); await tester.tap(find.byKey(sliderKey)); expect(value, equals(0.5)); await tester.pump(); expect(updates, equals(1)); }); testWidgets('Discrete Slider repaints when dragged', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; final log = []; final loggingThumb = LoggingThumbShape(log); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { final SliderThemeData sliderTheme = SliderTheme.of( context, ).copyWith(thumbShape: loggingThumb); return Material( child: Center( child: SliderTheme( data: sliderTheme, child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ); }, ), ), ), ); final expectedLog = [ const Offset(26.0, 300.0), const Offset(26.0, 300.0), const Offset(400.0, 300.0), ]; final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(value, equals(0.5)); expect(log.length, 3); expect(log, orderedEquals(expectedLog)); await gesture.moveBy(const Offset(-500.0, 0.0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 5); expect(log.last.dx, moreOrLessEquals(386.6, epsilon: 0.1)); // With no more gesture or value changes, the thumb position should still // be redrawn in the animated position. await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(value, equals(0.0)); expect(log.length, 7); expect(log.last.dx, moreOrLessEquals(344.8, epsilon: 0.1)); // Final position. await tester.pump(const Duration(milliseconds: 80)); expectedLog.add(const Offset(26.0, 300.0)); expect(value, equals(0.0)); expect(log.length, 8); expect(log.last.dx, moreOrLessEquals(26.0, epsilon: 0.1)); await gesture.up(); }); testWidgets('Slider take on discrete values', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: SizedBox( width: 144.0 + 2 * 16.0, // _kPreferredTotalWidth child: Slider( key: sliderKey, max: 100.0, divisions: 10, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey)); expect(value, equals(50.0)); await tester.drag(find.byKey(sliderKey), const Offset(5.0, 0.0)); expect(value, equals(50.0)); await tester.drag(find.byKey(sliderKey), const Offset(40.0, 0.0)); expect(value, equals(80.0)); await tester.pump(); // Starts animation. expect(SchedulerBinding.instance.transientCallbackCount, greaterThan(0)); await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200)); // Animation complete. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider can be given zero values', (WidgetTester tester) async { final log = []; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Slider( value: 0.0, onChanged: (double newValue) { log.add(newValue); }, ), ), ), ), ); await tester.tap(find.byType(Slider)); expect(log, [0.5]); log.clear(); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Slider( value: 0.0, max: 0.0, onChanged: (double newValue) { log.add(newValue); }, ), ), ), ), ); await tester.tap(find.byType(Slider)); expect(log, []); log.clear(); }); testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async { var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: ListView( children: [ Slider( value: value, onChanged: (double newValue) { value = newValue; }, ), Container(height: 2000.0), ], ), ), ), ), ); await tester.tap(find.byType(Slider)); expect(value, equals(0.5)); }); testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async { var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Slider( value: value, onChanged: (double newValue) { value = newValue; }, ), ), ), ), ), ); final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); expect(value, equals(0.5)); await gesture.moveBy(const Offset(1.0, 0.0)); expect(value, greaterThan(0.5)); await gesture.up(); }); testWidgets('Slider drags immediately (RTL)', (WidgetTester tester) async { var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.rtl, child: Material( child: Center( child: Slider( value: value, onChanged: (double newValue) { value = newValue; }, ), ), ), ), ), ); final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); expect(value, equals(0.5)); await gesture.moveBy(const Offset(1.0, 0.0)); expect(value, lessThan(0.5)); await gesture.up(); }); testWidgets('Slider onChangeStart and onChangeEnd fire once', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/28115 var startFired = 0; var endFired = 0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: GestureDetector( onHorizontalDragUpdate: (_) {}, child: Slider( value: 0.0, onChanged: (double newValue) {}, onChangeStart: (double value) { startFired += 1; }, onChangeEnd: (double value) { endFired += 1; }, ), ), ), ), ), ), ); await tester.timedDrag( find.byType(Slider), const Offset(20.0, 0.0), const Duration(milliseconds: 100), ); expect(startFired, equals(1)); expect(endFired, equals(1)); }); testWidgets('Slider sizing', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Center(child: Slider(value: 0.5, onChanged: null))), ), ), ); expect(tester.renderObject(find.byType(Slider)).size, const Size(800.0, 600.0)); await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center(child: IntrinsicWidth(child: Slider(value: 0.5, onChanged: null))), ), ), ), ); expect( tester.renderObject(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 600.0), ); await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: OverflowBox( maxWidth: double.infinity, maxHeight: double.infinity, child: Slider(value: 0.5, onChanged: null), ), ), ), ), ), ); expect( tester.renderObject(find.byType(Slider)).size, const Size(144.0 + 2.0 * 24.0, 48.0), ); }); testWidgets('Slider respects textScaleFactor', (WidgetTester tester) async { debugDisableShadows = false; try { final Key sliderKey = UniqueKey(); var value = 0.0; Widget buildSlider({ required double textScaleFactor, bool isDiscrete = true, ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete, }) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return MediaQuery( data: MediaQueryData(textScaler: TextScaler.linear(textScaleFactor)), child: Material( child: Theme( data: Theme.of(context).copyWith( sliderTheme: Theme.of( context, ).sliderTheme.copyWith(showValueIndicator: show), ), child: Center( child: OverflowBox( maxWidth: double.infinity, maxHeight: double.infinity, child: Slider( key: sliderKey, max: 100.0, divisions: isDiscrete ? 10 : null, label: '${value.round()}', value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ), ), ); }, ), ), ); } await tester.pumpWidget(buildSlider(textScaleFactor: 1.0)); Offset center = tester.getCenter(find.byType(Slider)); TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect( tester.renderObject(find.byType(Overlay)), paints ..path( includes: const [ Offset.zero, Offset(0.0, -8.0), Offset(-276.0, -16.0), Offset(-216.0, -16.0), ], color: const Color(0xf55f5f5f), ) ..paragraph(), ); await gesture.up(); await tester.pumpAndSettle(); await tester.pumpWidget(buildSlider(textScaleFactor: 2.0)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect( tester.renderObject(find.byType(Overlay)), paints ..path( includes: const [ Offset.zero, Offset(0.0, -8.0), Offset(-304.0, -16.0), Offset(-216.0, -16.0), ], color: const Color(0xf55f5f5f), ) ..paragraph(), ); await gesture.up(); await tester.pumpAndSettle(); // Check continuous await tester.pumpWidget( buildSlider( textScaleFactor: 1.0, isDiscrete: false, show: ShowValueIndicator.onlyForContinuous, ), ); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect( tester.renderObject(find.byType(Overlay)), paints ..path( includes: const [ Offset.zero, Offset(0.0, -8.0), Offset(-276.0, -16.0), Offset(-216.0, -16.0), ], color: const Color(0xf55f5f5f), ) ..paragraph(), ); await gesture.up(); await tester.pumpAndSettle(); await tester.pumpWidget( buildSlider( textScaleFactor: 2.0, isDiscrete: false, show: ShowValueIndicator.onlyForContinuous, ), ); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect( tester.renderObject(find.byType(Overlay)), paints ..path( includes: const [ Offset.zero, Offset(0.0, -8.0), Offset(-276.0, -16.0), Offset(-216.0, -16.0), ], color: const Color(0xf55f5f5f), ) ..paragraph(), ); await gesture.up(); await tester.pumpAndSettle(); } finally { debugDisableShadows = true; } }); testWidgets('Slider value indicator respects bold text', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; final log = []; final loggingValueIndicatorShape = LoggingValueIndicatorShape(log); Widget buildSlider({bool boldText = false}) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return MediaQuery( data: MediaQueryData(boldText: boldText), child: Material( child: Theme( data: Theme.of(context).copyWith( sliderTheme: Theme.of(context).sliderTheme.copyWith( showValueIndicator: ShowValueIndicator.always, valueIndicatorShape: loggingValueIndicatorShape, ), ), child: Center( child: OverflowBox( maxWidth: double.infinity, maxHeight: double.infinity, child: Slider( key: sliderKey, max: 100.0, divisions: 4, label: '${value.round()}', value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ), ), ); }, ), ), ); } // Normal text await tester.pumpWidget(buildSlider()); Offset center = tester.getCenter(find.byType(Slider)); TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect(log.last.toPlainText(), '50'); expect(log.last.style!.fontWeight, FontWeight.w500); await gesture.up(); await tester.pumpAndSettle(); // Bold text await tester.pumpWidget(buildSlider(boldText: true)); center = tester.getCenter(find.byType(Slider)); gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect(log.last.toPlainText(), '50'); expect(log.last.style!.fontWeight, FontWeight.w700); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Tick marks are skipped when they are too dense', (WidgetTester tester) async { Widget buildSlider({required int divisions}) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Slider( max: 100.0, divisions: divisions, value: 0.25, onChanged: (double newValue) {}, ), ), ), ), ); } // Pump a slider with a reasonable amount of divisions to verify that the // tick marks are drawn when the number of tick marks is not too dense. await tester.pumpWidget(buildSlider(divisions: 4)); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // 5 tick marks and a thumb. expect(material, paintsExactlyCountTimes(#drawCircle, 6)); // 200 divisions will produce a tick interval off less than 6, // which would be too dense to draw. await tester.pumpWidget(buildSlider(divisions: 200)); // No tick marks are drawn because they are too dense, but the thumb is // still drawn. expect(material, paintsExactlyCountTimes(#drawCircle, 1)); }); testWidgets('Slider has correct animations when reparented', (WidgetTester tester) async { final Key sliderKey = GlobalKey(debugLabel: 'A'); var value = 0.0; Widget buildSlider(int parents) { Widget createParents(int parents, StateSetter setState) { Widget slider = Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ); for (var i = 0; i < parents; ++i) { slider = Column(children: [slider]); } return slider; } return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material(child: createParents(parents, setState)); }, ), ), ); } Future testReparenting(bool reparent) async { final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); final Offset center = tester.getCenter(find.byType(Slider)); // Move to 0.0. TestGesture gesture = await tester.startGesture(Offset.zero); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect( material, paints ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 26.0, y: 24.0, radius: 10.0), ); gesture = await tester.startGesture(center); await tester.pump(); // Wait for animations to start. await tester.pump(const Duration(milliseconds: 25)); expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); expect( material, paints ..circle(x: 112.7431640625, y: 24.0, radius: 5.687664985656738) ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 112.7431640625, y: 24.0, radius: 10.0), ); // Reparenting in the middle of an animation should do nothing. if (reparent) { await tester.pumpWidget(buildSlider(2)); } // Move a little further in the animations. await tester.pump(const Duration(milliseconds: 10)); expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); expect( material, paints ..circle(x: 191.130521774292, y: 24.0, radius: 12.0) ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 191.130521774292, y: 24.0, radius: 10.0), ); // Wait for animations to finish. await tester.pumpAndSettle(); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect( material, paints ..circle(x: 400.0, y: 24.0, radius: 24.0) ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 10.0), ); await gesture.up(); await tester.pumpAndSettle(); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); expect( material, paints ..circle(x: 26.0, y: 24.0, radius: 1.0) ..circle(x: 213.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 1.0) ..circle(x: 587.0, y: 24.0, radius: 1.0) ..circle(x: 774.0, y: 24.0, radius: 1.0) ..circle(x: 400.0, y: 24.0, radius: 10.0), ); } await tester.pumpWidget(buildSlider(1)); // Do it once without reparenting in the middle of an animation await testReparenting(false); // Now do it again with reparenting in the middle of an animation. await testReparenting(true); }); testWidgets( 'Slider Semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 0.5, onChanged: (double v) {})), ), ), ); await tester.pumpAndSettle(); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, ], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); // Disable slider await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 0.5, onChanged: null)), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, // isFocusable is delayed by 1 frame. SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [SemanticsAction.focus], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pump(); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isSlider, ], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, }), skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS. ); testWidgets( 'Slider Semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Theme( data: ThemeData(), child: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 100.0, max: 200.0, onChanged: (double v) {})), ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ if (defaultTargetPlatform != TargetPlatform.iOS) SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, ], value: '50%', increasedValue: '60%', decreasedValue: '40%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); // Disable slider await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 0.5, onChanged: null)), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 6, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isSlider, ], value: '50%', increasedValue: '60%', decreasedValue: '40%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 7)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS, }), skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS. ); testWidgets( 'Slider Semantics', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 0.5, onChanged: (double v) {})), ), ), ); await tester.pumpAndSettle(); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, SemanticsAction.didGainAccessibilityFocus, ], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); // Disable slider await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material(child: Slider(value: 0.5, onChanged: null)), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, // isFocusable is delayed by 1 frame. SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.didGainAccessibilityFocus, ], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); await tester.pump(); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isSlider, ], actions: [SemanticsAction.didGainAccessibilityFocus], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }, variant: const TargetPlatformVariant({TargetPlatform.windows}), skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS. ); testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Slider( value: 40.0, max: 200.0, divisions: 10, semanticFormatterCallback: (double value) => value.round().toString(), onChanged: (double v) {}, ), ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, ], value: '40', increasedValue: '60', decreasedValue: '20', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS. // Regression test for https://github.com/flutter/flutter/issues/101868 testWidgets('Slider.label info should not write to semantic node', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const label = 'Bingo'; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Slider( value: 40.0, max: 200.0, divisions: 10, semanticFormatterCallback: (double value) => value.round().toString(), onChanged: (double v) {}, label: label, ), ), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, ], value: '40', increasedValue: '60', decreasedValue: '20', textDirection: TextDirection.ltr, label: label, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }, skip: kIsWeb); // [intended] the web traversal order by using ARIA-OWNS. testWidgets('Material3 - Slider is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Slider'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(); var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, autofocus: true, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Check that the overlay shows when focused. await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.1)), ); // Check that the overlay does not show when unfocused and disabled. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.1))), ); }); testWidgets('Slider has correct focus color from overlayColor property', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Slider'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, overlayColor: WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.focused)) { return Colors.purple[500]!; } return Colors.transparent; }), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, autofocus: true, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Check that the overlay shows when focused. await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: Colors.purple[500]), ); // Check that the overlay does not show when focused and disabled. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: Colors.purple[500])), ); }); testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(); var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not hovered. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: Colors.orange[500])), ); // Start hovering. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(Slider))); // Slider has overlay when enabled and hovered. await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.08)), ); // Slider still shows correct hovered color after pressing/dragging await gesture.down(tester.getCenter(find.byType(Slider))); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); await gesture.moveTo(const Offset(0.0, 100.0)); await tester.pumpAndSettle(); await gesture.moveTo(tester.getCenter(find.byType(Slider))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.08)), ); // Slider 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(Slider))), isNot(paints..circle(color: Colors.orange[500])), ); }); testWidgets('Slider has correct hovered color from overlayColor property', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, overlayColor: WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.hovered)) { return Colors.cyan[500]!; } return Colors.transparent; }), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not hovered. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: Colors.cyan[500])), ); // Start hovering. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getCenter(find.byType(Slider))); // Slider has overlay when enabled and hovered. await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: Colors.cyan[500]), ); // Slider 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(Slider))), isNot(paints..circle(color: Colors.cyan[500])), ); }); testWidgets('Material3 - Slider is draggable and has correct dragged color', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; final theme = ThemeData(); final Key sliderKey = UniqueKey(); final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( key: sliderKey, value: value, focusNode: focusNode, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not dragged. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.1))), ); // Start dragging. final 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(); // Slider has overlay when enabled and dragged. expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.1)), ); await drag.up(); await tester.pumpAndSettle(); // Slider without focus doesn't have overlay when enabled and dragged. expect(focusNode.hasFocus, false); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.1))), ); // Slider has overlay when enabled, dragged and focused. focusNode.requestFocus(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.1)), ); }); testWidgets('Slider has correct dragged color from overlayColor property', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; final Key sliderKey = UniqueKey(); final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( key: sliderKey, value: value, focusNode: focusNode, overlayColor: WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.dragged)) { return Colors.lime[500]!; } return Colors.transparent; }), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not dragged. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: Colors.lime[500])), ); // Start dragging. final 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(); // Slider has overlay when enabled and dragged. expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: Colors.lime[500]), ); await drag.up(); await tester.pumpAndSettle(); // Slider without focus doesn't have overlay when enabled and dragged. expect(focusNode.hasFocus, false); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: Colors.lime[500])), ); }); testWidgets('OverlayColor property is correctly applied when activeColor is also provided', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Slider'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; const activeColor = Color(0xffff0000); const overlayColor = Color(0xff0000ff); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, activeColor: activeColor, overlayColor: const MaterialStatePropertyAll(overlayColor), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Check that thumb color is using active color. expect(material, paints..circle(color: activeColor)); focusNode.requestFocus(); await tester.pumpAndSettle(); // Check that the overlay shows when focused. expect(focusNode.hasPrimaryFocus, isTrue); expect(Material.of(tester.element(find.byType(Slider))), paints..circle(color: overlayColor)); // Check that the overlay does not show when focused and disabled. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); }); testWidgets( 'Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var startValue = 0.0; var currentValue = 0.5; var endValue = 0.0; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: currentValue, onChangeStart: (double newValue) { setState(() { startValue = newValue; }); }, onChanged: (double newValue) { setState(() { currentValue = newValue; }); }, onChangeEnd: (double newValue) { setState(() { endValue = newValue; }); }, autofocus: true, ); }, ), ), ), ), ); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.55); expect(endValue, 0.55); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(startValue, 0.55); expect(currentValue, 0.5); expect(endValue, 0.5); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.55); expect(endValue, 0.55); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(startValue, 0.55); expect(currentValue, 0.5); expect(endValue, 0.5); }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, }), ); testWidgets( 'Slider can be incremented and decremented by keyboard shortcuts - LTR', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var startValue = 0.0; var currentValue = 0.5; var endValue = 0.0; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: currentValue, onChangeStart: (double newValue) { setState(() { startValue = newValue; }); }, onChanged: (double newValue) { setState(() { currentValue = newValue; }); }, onChangeEnd: (double newValue) { setState(() { endValue = newValue; }); }, autofocus: true, ); }, ), ), ), ), ); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.6); expect(endValue, 0.6); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(startValue, 0.6); expect(currentValue, 0.5); expect(endValue, 0.5); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.6); expect(endValue, 0.6); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(startValue, 0.6); expect(currentValue, 0.5); expect(endValue, 0.5); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS, }), ); testWidgets( 'Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var startValue = 0.0; var currentValue = 0.5; var endValue = 0.0; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Directionality( textDirection: TextDirection.rtl, child: Slider( value: currentValue, onChangeStart: (double newValue) { setState(() { startValue = newValue; }); }, onChanged: (double newValue) { setState(() { currentValue = newValue; }); }, onChangeEnd: (double newValue) { setState(() { endValue = newValue; }); }, autofocus: true, ), ); }, ), ), ), ), ); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.45); expect(endValue, 0.45); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(startValue, 0.45); expect(currentValue, 0.5); expect(endValue, 0.5); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.55); expect(endValue, 0.55); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(startValue, 0.55); expect(currentValue, 0.5); expect(endValue, 0.5); }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, }), ); testWidgets( 'Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var startValue = 0.0; var currentValue = 0.5; var endValue = 0.0; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Directionality( textDirection: TextDirection.rtl, child: Slider( value: currentValue, onChangeStart: (double newValue) { setState(() { startValue = newValue; }); }, onChanged: (double newValue) { setState(() { currentValue = newValue; }); }, onChangeEnd: (double newValue) { setState(() { endValue = newValue; }); }, autofocus: true, ), ); }, ), ), ), ), ); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.4); expect(endValue, 0.4); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(startValue, 0.4); expect(currentValue, 0.5); expect(endValue, 0.5); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(startValue, 0.5); expect(currentValue, 0.6); expect(endValue, 0.6); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(startValue, 0.6); expect(currentValue, 0.5); expect(endValue, 0.5); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS, }), ); testWidgets('In directional nav, Slider can be navigated out of by using up and down arrows', ( WidgetTester tester, ) async { const shortcuts = { SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent( TraversalDirection.left, ), SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent( TraversalDirection.right, ), SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent( TraversalDirection.down, ), SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up), }; tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var topSliderValue = 0.5; var bottomSliderValue = 0.5; await tester.pumpWidget( MaterialApp( home: Shortcuts( shortcuts: shortcuts, child: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return MediaQuery( data: const MediaQueryData(navigationMode: NavigationMode.directional), child: Column( children: [ Slider( value: topSliderValue, onChanged: (double newValue) { setState(() { topSliderValue = newValue; }); }, autofocus: true, ), Slider( value: bottomSliderValue, onChanged: (double newValue) { setState(() { bottomSliderValue = newValue; }); }, ), ], ), ); }, ), ), ), ), ), ); await tester.pumpAndSettle(); // The top slider is auto-focused and can be adjusted with left and right arrow keys. await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(topSliderValue, 0.55, reason: 'focused top Slider increased after first arrowRight'); expect( bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowRight', ); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after first arrowLeft'); expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowLeft'); // Pressing the down-arrow key moves focus down to the bottom slider await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'arrowDown unfocuses top Slider, does not alter its value'); expect( bottomSliderValue, 0.5, reason: 'arrowDown focuses bottom Slider, does not alter its value', ); // The bottom slider is now focused and can be adjusted with left and right arrow keys. await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowRight'); expect(bottomSliderValue, 0.55, reason: 'focused bottom Slider increased by second arrowRight'); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowLeft'); expect(bottomSliderValue, 0.5, reason: 'focused bottom Slider decreased by second arrowLeft'); // Pressing the up-arrow key moves focus back up to the top slider await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'arrowUp focuses top Slider, does not alter its value'); expect( bottomSliderValue, 0.5, reason: 'arrowUp unfocuses bottom Slider, does not alter its value', ); // The top slider is now focused again and can be adjusted with left and right arrow keys. await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(topSliderValue, 0.55, reason: 'focused top Slider increased after third arrowRight'); expect( bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight', ); await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after third arrowRight'); expect( bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight', ); }); testWidgets( 'Slider gains keyboard focus when it gains semantics focus on Windows', (WidgetTester tester) async { final semantics = SemanticsTester(tester); final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( home: Material( child: Slider(value: 0.5, onChanged: (double _) {}, focusNode: focusNode), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, flags: [ SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider, ], actions: [ SemanticsAction.focus, SemanticsAction.increase, SemanticsAction.decrease, SemanticsAction.didGainAccessibilityFocus, ], value: '50%', increasedValue: '55%', decreasedValue: '45%', textDirection: TextDirection.ltr, children: [TestSemantics(id: 5)], ), ], ), ], ), ], ), ], ), ignoreRect: true, ignoreTransform: true, ), ); expect(focusNode.hasFocus, isFalse); semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus); await tester.pumpAndSettle(); expect(focusNode.hasFocus, isTrue); semantics.dispose(); }, variant: const TargetPlatformVariant({TargetPlatform.windows}), skip: kIsWeb, // [intended] the web traversal order by using ARIA-OWNS. ); 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 = 0.45; Widget buildApp({required SliderThemeData sliderTheme, int? divisions, bool enabled = true}) { final ValueChanged? onChanged = enabled ? (double d) => value = d : null; return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Theme( data: baseTheme, child: SliderTheme( data: sliderTheme, child: Slider( value: value, label: '$value', divisions: divisions, onChanged: onChanged, ), ), ), ), ), ), ); } Future 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(Slider)); 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: 3); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false); await expectValueIndicator(isVisible: false, theme: theme); await expectValueIndicator(isVisible: false, theme: theme, enabled: false); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, 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: 3); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false); await expectValueIndicator(isVisible: true, theme: theme); await expectValueIndicator(isVisible: false, theme: theme, enabled: false); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, 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: 3); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false); await expectValueIndicator(isVisible: true, theme: theme); await expectValueIndicator(isVisible: false, theme: theme, enabled: false); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, 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: 3); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, enabled: false); await expectValueIndicator(isVisible: false, theme: theme); await expectValueIndicator(isVisible: false, theme: theme, enabled: false); await expectValueIndicator(isVisible: false, theme: theme, divisions: 3, 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("Slider doesn't start any animations after dispose", (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); await tester.pumpAndSettle(); expect(value, equals(0.5)); await gesture.moveBy(const Offset(-500.0, 0.0)); await tester.pumpAndSettle(); // Change the tree to dispose the original widget. await tester.pumpWidget(Container()); expect(await tester.pumpAndSettle(), equals(1)); await gesture.up(); }); testWidgets( 'Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); const fillColor = Color(0xf55f5f5f); var value = 0.0; Widget buildApp({int? divisions, bool enabled = true}) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: Builder( // 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. builder: (BuildContext context) { return Column( children: [ Slider( key: sliderKey, max: 100.0, divisions: divisions, label: '${value.round()}', value: value, onChanged: (double newValue) { value = newValue; }, ), ElevatedButton( child: const Text('Next'), onPressed: () { Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (BuildContext context) { return ElevatedButton( child: const Text('Inner page'), onPressed: () { Navigator.of(context).pop(); }, ); }, ), ); }, ), ], ); }, ), ), ); } await tester.pumpWidget(buildApp(divisions: 3)); final RenderObject valueIndicatorBox = tester.renderObject(find.byType(Overlay)); final Offset topRight = tester.getTopRight(find.byType(Slider)).translate(-24, 0); final TestGesture gesture = await tester.startGesture(topRight); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); expect(find.byType(Slider), isNotNull); expect( valueIndicatorBox, paints // Represents the raised button with text, next. ..path(color: Colors.black) ..paragraph() // Represents the Slider. ..path(color: fillColor) ..paragraph(), ); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 4)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 2)); await tester.tap(find.text('Next')); await tester.pumpAndSettle(); expect(find.byType(Slider), findsNothing); expect( valueIndicatorBox, isNot( paints ..path(color: fillColor) ..paragraph(), ), ); // Represents the ElevatedButton with inner Text, inner page. expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2)); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 1)); // Don't stop holding the value indicator. await gesture.up(); await tester.pumpAndSettle(); }, ); testWidgets('Slider.adaptive', (WidgetTester tester) async { var value = 0.5; Widget buildFrame(TargetPlatform platform) { return MaterialApp( theme: ThemeData(platform: platform), home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: Slider.adaptive( value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ); } for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { value = 0.5; await tester.pumpWidget(buildFrame(platform)); expect(find.byType(Slider), findsOneWidget); expect(find.byType(CupertinoSlider), findsOneWidget); expect(value, 0.5, reason: 'on ${platform.name}'); final TestGesture gesture = await tester.startGesture( tester.getCenter(find.byType(CupertinoSlider)), ); // Drag to the right end of the track. await gesture.moveBy(const Offset(600.0, 0.0)); expect(value, 1.0, reason: 'on ${platform.name}'); await gesture.up(); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { value = 0.5; await tester.pumpWidget(buildFrame(platform)); await tester.pumpAndSettle(); // Finish the theme change animation. expect(find.byType(Slider), findsOneWidget); expect(find.byType(CupertinoSlider), findsNothing); expect(value, 0.5, reason: 'on ${platform.name}'); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Slider))); // Drag to the right end of the track. await gesture.moveBy(const Offset(600.0, 0.0)); expect(value, 1.0, reason: 'on ${platform.name}'); await gesture.up(); } }); testWidgets('Slider respects height from theme', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); var value = 0.0; await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { final SliderThemeData sliderTheme = SliderTheme.of( context, ).copyWith(tickMarkShape: TallSliderTickMarkShape()); return Material( child: Center( child: IntrinsicHeight( child: SliderTheme( data: sliderTheme, child: Slider( key: sliderKey, value: value, divisions: 4, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ), ); }, ), ), ), ); final RenderBox renderObject = tester.renderObject(find.byType(Slider)); expect(renderObject.size.height, 200); }); testWidgets('Slider changes mouse cursor when hovered', (WidgetTester tester) async { // Test Slider() constructor await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Slider( mouseCursor: SystemMouseCursors.text, value: 0.5, onChanged: (double newValue) {}, ), ), ), ), ), ), ); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: tester.getCenter(find.byType(Slider))); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text, ); // Test Slider.adaptive() constructor await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Slider.adaptive( mouseCursor: SystemMouseCursors.text, value: 0.5, onChanged: (double newValue) {}, ), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text, ); // Test default cursor await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: MouseRegion( cursor: SystemMouseCursors.forbidden, child: Slider(value: 0.5, onChanged: (double newValue) {}), ), ), ), ), ), ); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click, ); }); testWidgets('Slider WidgetStateMouseCursor resolves correctly', (WidgetTester tester) async { const MouseCursor systemDefaultCursor = SystemMouseCursors.basic; const MouseCursor regularCursor = SystemMouseCursors.click; const MouseCursor disabledCursor = SystemMouseCursors.forbidden; const MouseCursor focusedCursor = SystemMouseCursors.precise; const MouseCursor hoveredCursor = SystemMouseCursors.grab; const MouseCursor draggedCursor = SystemMouseCursors.move; Widget buildFrame({required bool enabled}) { return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Center( child: Slider( mouseCursor: const _StateDependentMouseCursor( disabled: disabledCursor, focused: focusedCursor, hovered: hoveredCursor, dragged: draggedCursor, regular: regularCursor, ), value: 0.5, onChanged: enabled ? (double newValue) {} : 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.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), systemDefaultCursor); // Disabled. await tester.pumpWidget(buildFrame(enabled: false)); await gesture.moveTo(tester.getCenter(find.byType(Slider))); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor); // Regular. await tester.pumpWidget(buildFrame(enabled: true)); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), regularCursor); // Hovered. await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor); // Dragged. await gesture.down(tester.getCenter(find.byType(Slider))); 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); // Focused. await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), focusedCursor); // System default. await gesture.moveTo(Offset.zero); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), systemDefaultCursor); }); testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async { final builder = DiagnosticPropertiesBuilder(); const Slider( activeColor: Colors.blue, divisions: 10, inactiveColor: Colors.grey, secondaryActiveColor: Colors.blueGrey, label: 'Set a value', max: 100.0, onChanged: null, value: 50.0, secondaryTrackValue: 75.0, ).debugFillProperties(builder); final List description = builder.properties .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) .map((DiagnosticsNode node) => node.toString()) .toList(); expect(description, [ 'value: 50.0', 'secondaryTrackValue: 75.0', 'disabled', 'min: 0.0', 'max: 100.0', 'divisions: 10', 'label: "Set a value"', 'activeColor: MaterialColor(primary value: ${const Color(0xff2196f3)})', 'inactiveColor: MaterialColor(primary value: ${const Color(0xff9e9e9e)})', 'secondaryActiveColor: MaterialColor(primary value: ${const Color(0xff607d8b)})', ]); }); testWidgets('Slider track paints correctly when the shape is rectangular', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( theme: ThemeData( sliderTheme: const SliderThemeData(trackShape: RectangularSliderTrackShape()), ), home: const Directionality( textDirection: TextDirection.ltr, child: Material(child: Center(child: Slider(value: 0.5, onChanged: null))), ), ), ); final RenderObject renderObject = tester.allRenderObjects .where((RenderObject e) => e.runtimeType.toString() == '_RenderSlider') .first; // The active track rect should start at 24.0 pixels, // and there should not have a gap between active and inactive track. expect( renderObject, paints ..rect(rect: const Rect.fromLTRB(24.0, 298.0, 400.0, 302.0)) // active track Rect. ..rect(rect: const Rect.fromLTRB(400.0, 298.0, 776.0, 302.0)), // inactive track Rect. ); }); testWidgets('SliderTheme change should trigger re-layout', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/118955 var sliderValue = 0.0; Widget buildFrame(ThemeMode themeMode) { return MaterialApp( themeMode: themeMode, theme: ThemeData(brightness: Brightness.light), darkTheme: ThemeData(brightness: Brightness.dark), home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( height: 10.0, width: 10.0, child: Slider( value: sliderValue, label: 'label', onChanged: (double value) => sliderValue = value, ), ), ), ), ), ); } await tester.pumpWidget(buildFrame(ThemeMode.light)); final RenderObject renderObject = tester.allRenderObjects .where((RenderObject e) => e.runtimeType.toString() == '_RenderSlider') .first; expect(renderObject.debugNeedsLayout, false); await tester.pumpWidget(buildFrame(ThemeMode.dark)); await tester.pump( const Duration(milliseconds: 100), // to let the theme animate EnginePhase.build, ); expect(renderObject.debugNeedsLayout, true); // Pump the rest of the frames to complete the test. await tester.pumpAndSettle(); }); testWidgets('Slider can be painted in a narrower constraint', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: SizedBox( height: 10.0, width: 10.0, child: Slider(value: 0.5, onChanged: null), ), ), ), ), ), ); final RenderObject renderObject = tester.allRenderObjects .where((RenderObject e) => e.runtimeType.toString() == '_RenderSlider') .first; expect( renderObject, paints // Inactive track RRect. ..rrect(rrect: RRect.fromLTRBR(3.0, 3.0, 24.0, 7.0, const Radius.circular(2.0))) // Active track RRect. ..rrect(rrect: RRect.fromLTRBR(-14.0, 2.0, 7.0, 8.0, const Radius.circular(3.0))) // Thumb. ..circle(x: 5.0, y: 5.0, radius: 10.0), ); }); testWidgets('Update the divisions and value at the same time for Slider', ( WidgetTester tester, ) async { // Regress test for https://github.com/flutter/flutter/issues/65943 Widget buildFrame(double maxValue) { return MaterialApp( home: Material( child: Center( child: Slider.adaptive( value: 5, max: maxValue, divisions: maxValue.toInt(), onChanged: (double newValue) {}, ), ), ), ); } await tester.pumpWidget(buildFrame(10)); final RenderObject renderObject = tester.allRenderObjects .where((RenderObject e) => e.runtimeType.toString() == '_RenderSlider') .first; // Update the divisions from 10 to 15, the thumb 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() ..something((Symbol method, List arguments) { if (method != #drawRRect) { return false; } activeTrackRRect = arguments[0] as RRect; return true; }), ); const padding = 4.0; // The thumb should at one-third(5 / 15) of the Slider. // The right of the active track shape is the position of the thumb. // 24.0 is the default margin, (800.0 - 24.0 - 24.0) is the slider's width. expect( nearEqual( activeTrackRRect.right, (800.0 - 24.0 - 24.0 + (padding / 2)) * (5 / 15) + 24.0 + padding / 2, 0.01, ), true, ); }); testWidgets('Slider paints thumbColor', (WidgetTester tester) async { const color = Color(0xffffc107); final Widget sliderAdaptive = MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Material( child: Slider(value: 0, onChanged: (double newValue) {}, thumbColor: color), ), ); await tester.pumpWidget(sliderAdaptive); await tester.pumpAndSettle(); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect(material, paints..circle(color: color)); }); testWidgets('Slider.adaptive paints thumbColor on Android', (WidgetTester tester) async { const color = Color(0xffffc107); final Widget sliderAdaptive = MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Material( child: Slider.adaptive(value: 0, onChanged: (double newValue) {}, thumbColor: color), ), ); await tester.pumpWidget(sliderAdaptive); await tester.pumpAndSettle(); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect(material, paints..circle(color: color)); }); testWidgets('If thumbColor is null, it defaults to CupertinoColors.white', ( WidgetTester tester, ) async { final Widget sliderAdaptive = MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Material(child: Slider.adaptive(value: 0, onChanged: (double newValue) {})), ); await tester.pumpWidget(sliderAdaptive); await tester.pumpAndSettle(); final MaterialInkController material = Material.of( tester.element(find.byType(CupertinoSlider)), ); expect( material, paints ..rrect() ..rrect() ..rrect() ..rrect() ..rrect() ..rrect(color: CupertinoColors.white), ); }); testWidgets('Slider.adaptive passes thumbColor to CupertinoSlider', (WidgetTester tester) async { const color = Color(0xffffc107); final Widget sliderAdaptive = MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Material( child: Slider.adaptive(value: 0, onChanged: (double newValue) {}, thumbColor: color), ), ); await tester.pumpWidget(sliderAdaptive); await tester.pumpAndSettle(); final MaterialInkController material = Material.of( tester.element(find.byType(CupertinoSlider)), ); expect( material, paints ..rrect() ..rrect() ..rrect() ..rrect() ..rrect() ..rrect(color: color), ); }); // Regression test for https://github.com/flutter/flutter/issues/103566 testWidgets('Drag gesture uses provided gesture settings', (WidgetTester tester) async { var value = 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: Slider( value: value, key: sliderKey, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ), ), ); }, ), ), ), ); 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(value, 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: Slider( value: value, key: sliderKey, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeEnd: (double endValue) { 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); }); // Regression test for https://github.com/flutter/flutter/issues/139281 testWidgets('Slider does not request focus when the value is changed', ( WidgetTester tester, ) async { var value = 0.5; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ); }, ), ), ), ), ); // Initially, the slider does not have focus whe enabled and not tapped. await tester.pumpAndSettle(); expect(value, equals(0.5)); // Get FocusNode from the state of the slider to include auto-generated FocusNode. // ignore: invalid_assignment final FocusNode focusNode = (tester.firstState(find.byType(Slider)) as dynamic).focusNode; // The slider does not have focus. expect(focusNode.hasFocus, false); final Offset sliderCenter = tester.getCenter(find.byType(Slider)); final tapLocation = Offset(sliderCenter.dx + 50, sliderCenter.dy); // Tap on the slider to change the value. final TestGesture gesture = await tester.createGesture(); await gesture.addPointer(); await gesture.down(tapLocation); await gesture.up(); await tester.pumpAndSettle(); expect(value, isNot(equals(0.5))); // The slider does not have focus after the value is changed. expect(focusNode.hasFocus, false); }); // Regression test for https://github.com/flutter/flutter/issues/139281 testWidgets('Overlay remains when Slider thumb is interacted', (WidgetTester tester) async { var value = 0.5; const overlayColor = Color(0xffff0000); await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, overlayColor: const MaterialStatePropertyAll(overlayColor), onChanged: (double newValue) { setState(() { value = newValue; }); }, ); }, ), ), ), ), ); // Slider does not have overlay when enabled and not tapped. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); final Offset sliderCenter = tester.getCenter(find.byType(Slider)); // Tap and hold down on the thumb to keep it active. final TestGesture gesture = await tester.createGesture(); await gesture.addPointer(); await gesture.down(sliderCenter); await tester.pumpAndSettle(); expect(Material.of(tester.element(find.byType(Slider))), paints..circle(color: overlayColor)); // Hover on the slider but outside the thumb. await gesture.moveTo(tester.getTopLeft(find.byType(Slider))); await tester.pumpAndSettle(); expect(Material.of(tester.element(find.byType(Slider))), paints..circle(color: overlayColor)); // Tap up on the slider. await gesture.up(); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); }); testWidgets( 'Overlay appear only when hovered on the thumb on desktop', (WidgetTester tester) async { var value = 0.5; const overlayColor = Color(0xffff0000); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, overlayColor: const MaterialStatePropertyAll(overlayColor), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not hovered. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); // Hover on the slider but outside the thumb. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(tester.getTopLeft(find.byType(Slider))); await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); // Hover on the thumb. await gesture.moveTo(tester.getCenter(find.byType(Slider))); await tester.pumpAndSettle(); expect(Material.of(tester.element(find.byType(Slider))), paints..circle(color: overlayColor)); // Hover on the slider but outside the thumb. await gesture.moveTo(tester.getBottomRight(find.byType(Slider))); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); }, variant: TargetPlatformVariant.desktop(), ); testWidgets('Overlay remains when Slider is in focus on desktop', (WidgetTester tester) async { var value = 0.5; const overlayColor = Color(0xffff0000); final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, focusNode: focusNode, overlayColor: const MaterialStatePropertyAll(overlayColor), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not tapped. await tester.pumpAndSettle(); expect(focusNode.hasFocus, false); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); final Offset sliderCenter = tester.getCenter(find.byType(Slider)); var tapLocation = Offset(sliderCenter.dx + 50, sliderCenter.dy); // Tap somewhere to bring overlay. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.down(tapLocation); await gesture.up(); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); expect(Material.of(tester.element(find.byType(Slider))), paints..circle(color: overlayColor)); tapLocation = Offset(sliderCenter.dx - 50, sliderCenter.dy); await gesture.down(tapLocation); await gesture.up(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); // Overlay is removed when adjusted with a tap. expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: overlayColor)), ); }, variant: TargetPlatformVariant.desktop()); // Regression test for https://github.com/flutter/flutter/issues/123313, which only occurs on desktop platforms. testWidgets( 'Value indicator disappears after adjusting the slider on desktop', (WidgetTester tester) async { final theme = ThemeData(); const currentValue = 0.5; await tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: Center( child: Slider( value: currentValue, divisions: 5, label: currentValue.toStringAsFixed(1), onChanged: (_) {}, ), ), ), ), ); // Slider does not show value indicator initially. await tester.pumpAndSettle(); RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, isNot( paints ..scale() ..path(color: theme.colorScheme.primary), ), ); final Offset sliderCenter = tester.getCenter(find.byType(Slider)); final tapLocation = Offset(sliderCenter.dx + 50, sliderCenter.dy); // Tap the slider by mouse to bring up the value indicator. await tester.tapAt(tapLocation, kind: PointerDeviceKind.mouse); await tester.pumpAndSettle(); // Value indicator is visible. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); // Wait for the value indicator to disappear. await tester.pumpAndSettle(const Duration(seconds: 2)); // Value indicator is no longer visible. expect( valueIndicatorBox, isNot( paints ..scale() ..path(color: theme.colorScheme.primary), ), ); }, variant: TargetPlatformVariant.desktop(), ); testWidgets( 'Value indicator remains when Slider is in focus on desktop', (WidgetTester tester) async { var value = 0.5; final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: ThemeData( sliderTheme: const SliderThemeData(showValueIndicator: ShowValueIndicator.always), ), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, focusNode: focusNode, divisions: 5, label: value.toStringAsFixed(1), onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not show value indicator without focus. await tester.pumpAndSettle(); expect(focusNode.hasFocus, false); RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, isNot( paints ..path(color: const Color(0xff000000)) ..paragraph(), ), ); final Offset sliderCenter = tester.getCenter(find.byType(Slider)); final tapLocation = Offset(sliderCenter.dx + 50, sliderCenter.dy); // Tap somewhere to bring value indicator. final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.down(tapLocation); await gesture.up(); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..path(color: const Color(0xff000000)) ..paragraph(), ); focusNode.unfocus(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, false); expect( valueIndicatorBox, isNot( paints ..path(color: const Color(0xff000000)) ..paragraph(), ), ); }, variant: TargetPlatformVariant.desktop(), ); testWidgets('showValueIndicator takes priority over theme', (WidgetTester tester) async { Widget buildApp({ required ShowValueIndicator? themeShowValueIndicator, required ShowValueIndicator? sliderShowValueIndicator, }) { return MaterialApp( home: Material( child: Center( child: SliderTheme( data: SliderThemeData( valueIndicatorColor: Colors.red, showValueIndicator: themeShowValueIndicator, ), child: Slider( value: 0.5, label: '0.5', onChanged: (double newValue) {}, showValueIndicator: sliderShowValueIndicator, ), ), ), ), ); } void checkValueIndicator({required bool isVisible}) { // _RenderValueIndicator is the last render object in the tree. final RenderObject valueIndicatorBox = tester.allRenderObjects.last; final PaintPattern matcher = paints ..path(color: Colors.red) ..paragraph(); expect(valueIndicatorBox, isVisible ? matcher : isNot(matcher)); } await tester.pumpWidget( buildApp(themeShowValueIndicator: ShowValueIndicator.never, sliderShowValueIndicator: null), ); checkValueIndicator(isVisible: false); await tester.pumpWidget( buildApp( themeShowValueIndicator: ShowValueIndicator.never, sliderShowValueIndicator: ShowValueIndicator.alwaysVisible, ), ); checkValueIndicator(isVisible: true); await tester.pumpWidget( buildApp( themeShowValueIndicator: ShowValueIndicator.alwaysVisible, sliderShowValueIndicator: ShowValueIndicator.never, ), ); checkValueIndicator(isVisible: false); }); testWidgets('Event on Slider should perform no-op if already unmounted', ( WidgetTester tester, ) async { // Test covering crashing found in Google internal issue b/192329942. var value = 0.0; final shouldShowSliderListenable = ValueNotifier(true); addTearDown(shouldShowSliderListenable.dispose); await tester.pumpWidget( MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: ValueListenableBuilder( valueListenable: shouldShowSliderListenable, builder: (BuildContext context, bool shouldShowSlider, _) { return GestureDetector( behavior: HitTestBehavior.translucent, // Note: it is important that `onTap` is non-null so // [GestureDetector] will register tap events. onTap: () {}, child: shouldShowSlider ? Slider( value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ) : const SizedBox.expand(), ); }, ), ), ); }, ), ), ), ); // Move Slider. final TestGesture gesture = await tester.startGesture( tester.getRect(find.byType(Slider)).center, ); await gesture.moveBy(const Offset(1.0, 0.0)); await tester.pumpAndSettle(); // Hide Slider. Slider will dispose and unmount. shouldShowSliderListenable.value = false; await tester.pumpAndSettle(); // Move Slider after unmounted. await gesture.moveBy(const Offset(1.0, 0.0)); await tester.pumpAndSettle(); expect(tester.takeException(), null); }); group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests // can be deleted. testWidgets('Slider can be hovered and has correct hover color', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(useMaterial3: false); var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not hovered. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), 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(Slider))); // Slider has overlay when enabled and hovered. await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), ); // Slider 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(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), ); }); testWidgets('Material2 - Slider is focusable and has correct focus color', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Slider'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(useMaterial3: false); var value = 0.5; Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, autofocus: true, focusNode: focusNode, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Check that the overlay shows when focused. await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isTrue); expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), ); // Check that the overlay does not show when unfocused and disabled. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect(focusNode.hasPrimaryFocus, isFalse); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), ); }); testWidgets('Material2 - Slider is draggable and has correct dragged color', ( WidgetTester tester, ) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = 0.5; final theme = ThemeData(useMaterial3: false); final Key sliderKey = UniqueKey(); final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp({bool enabled = true}) { return MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( key: sliderKey, value: value, focusNode: focusNode, onChanged: enabled ? (double newValue) { setState(() { value = newValue; }); } : null, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not have overlay when enabled and not dragged. await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), ); // Start dragging. final 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(); // Slider has overlay when enabled and dragged. expect( Material.of(tester.element(find.byType(Slider))), paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), ); await drag.up(); await tester.pumpAndSettle(); // Slider without focus doesn't have overlay when enabled and dragged. expect(focusNode.hasFocus, false); expect( Material.of(tester.element(find.byType(Slider))), isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), ); }); }); group('Slider.allowedInteraction', () { testWidgets('SliderInteraction.tapOnly', (WidgetTester tester) async { var value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) const startOfTheSliderTrack = Offset(24, 300); const centerOfTheSlideTrack = Offset(400, 300); final logs = []; Widget buildWidget() => MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext _, StateSetter setState) { return Slider( value: value, key: sliderKey, allowedInteraction: SliderInteraction.tapOnly, onChangeStart: (double newValue) { logs.add('onChangeStart'); }, onChanged: (double newValue) { logs.add('onChanged'); setState(() { value = newValue; }); }, onChangeEnd: (double newValue) { logs.add('onChangeEnd'); }, ); }, ), ), ), ); // allow tap only await tester.pumpWidget(buildWidget()); expect(logs, isEmpty); // test tap final TestGesture gesture = await tester.startGesture(centerOfTheSlideTrack); await tester.pump(); // changes from 1.0 -> 0.5 expect(value, 0.5); expect(logs, ['onChangeStart', 'onChanged']); // test slide await gesture.moveTo(startOfTheSliderTrack); await tester.pump(); // has no effect, remains 0.5 expect(value, 0.5); expect(logs, ['onChangeStart', 'onChanged']); await gesture.up(); await tester.pump(); expect(logs, ['onChangeStart', 'onChanged', 'onChangeEnd']); }); testWidgets('SliderInteraction.tapAndSlide (default)', (WidgetTester tester) async { var value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) const startOfTheSliderTrack = Offset(24, 300); const centerOfTheSlideTrack = Offset(400, 300); const endOfTheSliderTrack = Offset(800 - 24, 300); final logs = []; Widget buildWidget() => MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext _, StateSetter setState) { return Slider( value: value, key: sliderKey, onChangeStart: (double newValue) { logs.add('onChangeStart'); }, onChanged: (double newValue) { logs.add('onChanged'); setState(() { value = newValue; }); }, onChangeEnd: (double newValue) { logs.add('onChangeEnd'); }, ); }, ), ), ), ); await tester.pumpWidget(buildWidget()); expect(logs, isEmpty); // Test tap. final TestGesture gesture = await tester.startGesture(centerOfTheSlideTrack); await tester.pump(); // changes from 1.0 -> 0.5 expect(value, 0.5); expect(logs, ['onChangeStart', 'onChanged']); // test slide await gesture.moveTo(startOfTheSliderTrack); await tester.pump(); // changes from 0.5 -> 0.0 expect(value, 0.0); await gesture.moveTo(endOfTheSliderTrack); await tester.pump(); // changes from 0.0 -> 1.0 expect(value, 1.0); expect(logs, ['onChangeStart', 'onChanged', 'onChanged', 'onChanged']); await gesture.up(); await tester.pump(); expect(logs, ['onChangeStart', 'onChanged', 'onChanged', 'onChanged', 'onChangeEnd']); }); testWidgets('SliderInteraction.slideOnly', (WidgetTester tester) async { const double overlayRadius = 23; const Color overlayColor = Colors.red; var value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) const startOfTheSliderTrack = Offset(overlayRadius, 300); const centerOfTheSliderTrack = Offset(400, 300); const endOfTheSliderTrack = Offset(800 - overlayRadius, 300); final xPosThumb = Tween(begin: startOfTheSliderTrack.dx, end: endOfTheSliderTrack.dx); final logs = []; Widget buildApp() { return MaterialApp( theme: ThemeData( sliderTheme: const SliderThemeData( overlayColor: overlayColor, overlayShape: RoundSliderOverlayShape(overlayRadius: overlayRadius), ), ), home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext _, StateSetter setState) { return Slider( value: value, key: sliderKey, allowedInteraction: SliderInteraction.slideOnly, onChangeStart: (double newValue) { logs.add('onChangeStart'); }, onChanged: (double newValue) { logs.add('onChanged'); setState(() { value = newValue; }); }, onChangeEnd: (double newValue) { logs.add('onChangeEnd'); }, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); final Element sliderContext = tester.element(find.byType(Slider)); final MaterialInkController material = Material.of(sliderContext); expect(logs, isEmpty); // Test tap. final TestGesture gesture = await tester.startGesture(centerOfTheSliderTrack); // Start animation. await tester.pump(); // Go to mid-animation frame. await tester.pump(kRadialReactionDuration * 0.5); expect(material, paints..circle(color: overlayColor, x: xPosThumb.transform(value))); // We have a non-linear asymmetric curve, so just verify the radius is not full. expect( material, isNot( paints..circle(color: overlayColor, radius: overlayRadius, x: xPosThumb.transform(value)), ), ); // Finish animation. await tester.pumpAndSettle(); // Overlay drawn. expect( material, paints..circle(color: overlayColor, radius: overlayRadius, x: xPosThumb.transform(value)), ); // Has no other effect as tap is disabled, remains 1.0. expect(value, 1.0); expect(logs, ['onChangeStart']); // Test slide. await gesture.moveTo(startOfTheSliderTrack); await tester.pump(); // Changes from 1.0 -> 0.5. expect(value, 0.5); // Overlay still there. expect( material, paints..circle(color: overlayColor, radius: overlayRadius, x: xPosThumb.transform(value)), ); await gesture.moveTo(endOfTheSliderTrack); await tester.pump(); // Changes from 0.0 -> 1.0. expect(value, 1.0); expect(logs, ['onChangeStart', 'onChanged', 'onChanged']); // Overlay still there. expect( material, paints..circle(color: overlayColor, radius: overlayRadius, x: xPosThumb.transform(value)), ); await gesture.up(); // Start release animation. await tester.pump(); // Go to mid-animation frame. await tester.pump(kRadialReactionDuration * 0.5); expect(material, paints..circle(color: overlayColor, x: xPosThumb.transform(value))); // Verify the radius is not full. expect( material, isNot( paints..circle(color: overlayColor, radius: overlayRadius, x: xPosThumb.transform(value)), ), ); await tester.pumpAndSettle(); // No overlay drawn. expect(material, isNot(paints..circle(color: overlayColor, radius: overlayRadius))); expect(logs, ['onChangeStart', 'onChanged', 'onChanged', 'onChangeEnd']); }); testWidgets('SliderInteraction.slideThumb', (WidgetTester tester) async { var value = 1.0; final Key sliderKey = UniqueKey(); // (slider's left padding (overlayRadius), windowHeight / 2) const startOfTheSliderTrack = Offset(24, 300); const centerOfTheSliderTrack = Offset(400, 300); const endOfTheSliderTrack = Offset(800 - 24, 300); final logs = []; Widget buildApp() { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext _, StateSetter setState) { return Slider( value: value, key: sliderKey, allowedInteraction: SliderInteraction.slideThumb, onChangeStart: (double newValue) { logs.add('onChangeStart'); }, onChanged: (double newValue) { logs.add('onChanged'); setState(() { value = newValue; }); }, onChangeEnd: (double newValue) { logs.add('onChangeEnd'); }, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); expect(logs, isEmpty); // test tap final TestGesture gesture = await tester.startGesture(centerOfTheSliderTrack); await tester.pump(); // has no effect, remains 1.0 expect(value, 1.0); expect(logs, isEmpty); // test slide await gesture.moveTo(startOfTheSliderTrack); await tester.pump(); // has no effect, remains 1.0 expect(value, 1.0); expect(logs, isEmpty); // test slide thumb await gesture.up(); await gesture.down(endOfTheSliderTrack); // where the thumb is await tester.pump(); // has no effect, remains 1.0 expect(value, 1.0); expect(logs, ['onChangeStart']); await gesture.moveTo(centerOfTheSliderTrack); await tester.pump(); // changes from 1.0 -> 0.5 expect(value, 0.5); expect(logs, ['onChangeStart', 'onChanged']); // test tap inside overlay but not on thumb, then slide await gesture.up(); // default overlay radius is 12, so 10 is inside the overlay await gesture.down(centerOfTheSliderTrack.translate(-10, 0)); await tester.pump(); // changes from 1.0 -> 0.5 expect(value, 0.5); expect(logs, ['onChangeStart', 'onChanged', 'onChangeEnd', 'onChangeStart']); await gesture.moveTo(endOfTheSliderTrack.translate(-10, 0)); await tester.pump(); // changes from 0.5 -> 1.0 expect(value, 1.0); expect(logs, [ 'onChangeStart', 'onChanged', 'onChangeEnd', 'onChangeStart', 'onChanged', ]); await gesture.up(); await tester.pump(); expect(logs, [ 'onChangeStart', 'onChanged', 'onChangeEnd', 'onChangeStart', 'onChanged', 'onChangeEnd', ]); }); }); // This is a regression test for https://github.com/flutter/flutter/issues/143524. testWidgets('Discrete Slider.onChanged is called only once', (WidgetTester tester) async { var onChangeCallbackCount = 0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: Slider( max: 5, divisions: 5, value: 0, onChanged: (double newValue) { onChangeCallbackCount++; }, ), ), ), ), ); final TestGesture gesture = await tester.startGesture(tester.getTopLeft(find.byType(Slider))); await tester.pump(kLongPressTimeout); await gesture.moveBy(const Offset(160.0, 0.0)); await gesture.moveBy(const Offset(1.0, 0.0)); await gesture.moveBy(const Offset(1.0, 0.0)); expect(onChangeCallbackCount, 1); }); testWidgets('Skip drawing ValueIndicator shape when label painter text is null', ( WidgetTester tester, ) async { double sliderValue = 10; await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, void Function(void Function()) setState) { return Material( child: Slider( value: sliderValue, max: 100, label: sliderValue > 50 ? null : sliderValue.toString(), divisions: 10, onChanged: (double value) { setState(() { sliderValue = value; }); }, ), ); }, ), ), ); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); // Calculate a specific position on the Slider. final Rect sliderRect = tester.getRect(find.byType(Slider)); final tapPositionLeft = Offset(sliderRect.left + sliderRect.width * 0.25, sliderRect.center.dy); final tapPositionRight = Offset( sliderRect.left + sliderRect.width * 0.75, sliderRect.center.dy, ); // Tap on the 25% position of the Slider. await tester.tapAt(tapPositionLeft); await tester.pumpAndSettle(); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2)); // Tap on the 75% position of the Slider. await tester.tapAt(tapPositionRight); await tester.pumpAndSettle(); expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 1)); }); testWidgets('Slider value indicator is shown when using arrow keys', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final theme = ThemeData(); var startValue = 0.0; var currentValue = 0.5; var endValue = 0.0; await tester.pumpWidget( MaterialApp( theme: theme, home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: currentValue, divisions: 5, label: currentValue.toStringAsFixed(1), onChangeStart: (double newValue) { setState(() { startValue = newValue; }); }, onChanged: (double newValue) { setState(() { currentValue = newValue; }); }, onChangeEnd: (double newValue) { setState(() { endValue = newValue; }); }, autofocus: true, ); }, ), ), ), ), ); // Slider shows value indicator initially on focus. await tester.pumpAndSettle(); RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); // Right arrow (increase) await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); expect(startValue, 0.6); expect(currentValue.toStringAsFixed(1), '0.8'); expect(endValue.toStringAsFixed(1), '0.8'); // Value indicator is visible. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); // Left arrow (decrease) await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); await tester.pumpAndSettle(); expect(startValue, 0.8); expect(currentValue.toStringAsFixed(1), '0.6'); expect(endValue.toStringAsFixed(1), '0.6'); // Value indicator is still visible. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); // Up arrow (increase) await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.pumpAndSettle(); expect(startValue, 0.6); expect(currentValue.toStringAsFixed(1), '0.8'); expect(endValue.toStringAsFixed(1), '0.8'); // Value indicator is still visible. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); // Down arrow (decrease) await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.pumpAndSettle(); expect(startValue, 0.8); expect(currentValue.toStringAsFixed(1), '0.6'); expect(endValue.toStringAsFixed(1), '0.6'); // Value indicator is still visible. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..path(color: theme.colorScheme.primary), ); }, variant: TargetPlatformVariant.desktop()); testWidgets('Value indicator label is shown when focused', (WidgetTester tester) async { var value = 0.5; final focusNode = FocusNode(); addTearDown(focusNode.dispose); Widget buildApp() { return MaterialApp( home: Material( child: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Slider( value: value, focusNode: focusNode, divisions: 5, label: value.toStringAsFixed(1), onChanged: (double newValue) { setState(() { value = newValue; }); }, ); }, ), ), ), ); } await tester.pumpWidget(buildApp()); // Slider does not show value indicator without focus. await tester.pumpAndSettle(); expect(focusNode.hasFocus, false); RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, isNot( paints ..path(color: const Color(0xff000000)) ..paragraph(), ), ); focusNode.requestFocus(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); // Slider shows value indicator when focused. valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..path(color: const Color(0xff000000)) ..paragraph(), ); }, variant: TargetPlatformVariant.desktop()); testWidgets('Slider.padding can override the default Slider padding', ( WidgetTester tester, ) async { Widget buildSlider({EdgeInsetsGeometry? padding}) { return MaterialApp( home: Material( child: Center( child: IntrinsicHeight( child: Slider(padding: padding, value: 0.5, onChanged: (double value) {}), ), ), ), ); } RenderBox sliderRenderBox() { return tester.allRenderObjects.firstWhere( (RenderObject object) => object.runtimeType.toString() == '_RenderSlider', ) as RenderBox; } // Test Slider height and tracks spacing with zero padding. await tester.pumpWidget(buildSlider(padding: EdgeInsets.zero)); await tester.pumpAndSettle(); // The height equals to the default thumb height. expect(sliderRenderBox().size, const Size(800, 20)); expect( find.byType(Slider), paints // Inactive track. ..rrect(rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0))) // Active track. ..rrect(rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0))), ); // Test Slider height and tracks spacing with directional padding. const double startPadding = 100; const double endPadding = 20; await tester.pumpWidget( buildSlider( padding: const EdgeInsetsDirectional.only(start: startPadding, end: endPadding), ), ); await tester.pumpAndSettle(); expect(sliderRenderBox().size, const Size(800 - startPadding - endPadding, 20)); expect( find.byType(Slider), paints // Inactive track. ..rrect(rrect: RRect.fromLTRBR(338.0, 8.0, 680.0, 12.0, const Radius.circular(2.0))) // Active track. ..rrect(rrect: RRect.fromLTRBR(0.0, 7.0, 342.0, 13.0, const Radius.circular(3.0))), ); // Test Slider height and tracks spacing with top and bottom padding. const double topPadding = 100; const double bottomPadding = 20; const double trackHeight = 20; await tester.pumpWidget( buildSlider( padding: const EdgeInsetsDirectional.only(top: topPadding, bottom: bottomPadding), ), ); await tester.pumpAndSettle(); expect( tester.getSize(find.byType(Slider)), const Size(800, topPadding + trackHeight + bottomPadding), ); expect(sliderRenderBox().size, const Size(800, 20)); expect( find.byType(Slider), paints // Inactive track. ..rrect(rrect: RRect.fromLTRBR(398.0, 8.0, 800.0, 12.0, const Radius.circular(2.0))) // Active track. ..rrect(rrect: RRect.fromLTRBR(0.0, 7.0, 402.0, 13.0, const Radius.circular(3.0))), ); }); testWidgets('Default Slider when year2023 is false', (WidgetTester tester) async { debugDisableShadows = false; try { final theme = ThemeData(); final ColorScheme colorScheme = theme.colorScheme; final Color activeTrackColor = colorScheme.primary; final Color inactiveTrackColor = colorScheme.secondaryContainer; final Color secondaryActiveTrackColor = colorScheme.primary.withOpacity(0.54); final Color disabledActiveTrackColor = colorScheme.onSurface.withOpacity(0.38); final Color disabledInactiveTrackColor = colorScheme.onSurface.withOpacity(0.12); final Color disabledSecondaryActiveTrackColor = colorScheme.onSurface.withOpacity(0.38); 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 value = 0.45; Widget buildApp({int? divisions, bool enabled = true}) { final ValueChanged? onChanged = !enabled ? null : (double d) { value = d; }; return MaterialApp( home: Directionality( textDirection: TextDirection.ltr, child: Material( child: Center( child: Theme( data: theme, child: Slider( year2023: false, value: value, secondaryTrackValue: 0.75, label: '$value', divisions: divisions, onChanged: onChanged, ), ), ), ), ), ); } await tester.pumpWidget(buildApp()); final MaterialInkController material = Material.of(tester.element(find.byType(Slider))); // Test default track shape. const trackOuterCornerRadius = Radius.circular(8.0); const trackInnerCornerRadius = Radius.circular(2.0); expect( material, paints // Active track. ..rrect( rrect: RRect.fromLTRBAndCorners( 24.0, 292.0, 356.4, 308.0, topLeft: trackOuterCornerRadius, topRight: trackInnerCornerRadius, bottomRight: trackInnerCornerRadius, bottomLeft: trackOuterCornerRadius, ), color: activeTrackColor, ) // Inactive track. ..rrect( rrect: RRect.fromLTRBAndCorners( 368.4, 292.0, 776.0, 308.0, topLeft: trackInnerCornerRadius, topRight: trackOuterCornerRadius, bottomRight: trackOuterCornerRadius, bottomLeft: trackInnerCornerRadius, ), color: inactiveTrackColor, ), ); // Test default colors for enabled slider. expect( material, paints ..circle() ..rrect(color: thumbColor), ); expect( material, isNot( paints ..circle() ..circle(color: disabledThumbColor), ), ); expect(material, isNot(paints..rrect(color: disabledActiveTrackColor))); expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor))); expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor))); // Test defaults colors for discrete slider. await tester.pumpWidget(buildApp(divisions: 3)); expect( material, paints ..rrect(color: activeTrackColor) ..rrect(color: inactiveTrackColor) ..rrect(color: secondaryActiveTrackColor) ..circle(color: activeTickMarkColor) ..circle(color: activeTickMarkColor) ..circle(color: inactiveTickMarkColor) ..circle(color: inactiveTickMarkColor), ); expect(material, isNot(paints..circle(color: disabledThumbColor))); expect(material, isNot(paints..rrect(color: disabledActiveTrackColor))); expect(material, isNot(paints..rrect(color: disabledInactiveTrackColor))); expect(material, isNot(paints..rrect(color: disabledSecondaryActiveTrackColor))); // Test defaults colors for disabled slider. await tester.pumpWidget(buildApp(enabled: false)); await tester.pumpAndSettle(); expect( material, paints ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledInactiveTrackColor) ..rrect(color: disabledSecondaryActiveTrackColor), ); expect( material, paints ..circle() ..rrect(color: disabledThumbColor), ); expect( material, isNot( paints ..circle() ..rrect(color: thumbColor), ), ); expect(material, isNot(paints..rrect(color: activeTrackColor))); expect(material, isNot(paints..rrect(color: inactiveTrackColor))); expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor))); // Test defaults colors for disabled discrete slider. await tester.pumpWidget(buildApp(divisions: 3, enabled: false)); expect( material, paints ..rrect(color: disabledActiveTrackColor) ..rrect(color: disabledInactiveTrackColor) ..rrect(color: disabledSecondaryActiveTrackColor) ..circle(color: disabledActiveTickMarkColor) ..circle(color: disabledActiveTickMarkColor) ..circle(color: disabledInactiveTickMarkColor) ..circle(color: disabledInactiveTickMarkColor) ..rrect(color: disabledThumbColor), ); expect( material, isNot( paints ..circle() ..rrect(color: thumbColor), ), ); expect(material, isNot(paints..rrect(color: activeTrackColor))); expect(material, isNot(paints..rrect(color: inactiveTrackColor))); expect(material, isNot(paints..rrect(color: secondaryActiveTrackColor))); await tester.pumpWidget(buildApp(divisions: 3)); await tester.pumpAndSettle(); final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); // Wait for value indicator animation to finish. await tester.pumpAndSettle(); final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); expect( valueIndicatorBox, paints ..scale() ..rrect(color: valueIndicatorColor), ); await gesture.up(); } finally { debugDisableShadows = true; } }); testWidgets('Slider value indicator text when year2023 is false', (WidgetTester tester) async { const double value = 50; final log = []; final loggingValueIndicatorShape = LoggingValueIndicatorShape(log); final theme = ThemeData( sliderTheme: SliderThemeData(valueIndicatorShape: loggingValueIndicatorShape), ); Widget buildSlider() { return MaterialApp( theme: theme, home: Material( child: Center( child: Slider( year2023: false, max: 100.0, divisions: 4, label: '${value.round()}', value: value, onChanged: (double newValue) {}, ), ), ), ); } // Normal text await tester.pumpWidget(buildSlider()); final Offset center = tester.getCenter(find.byType(Slider)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); expect(log.last.toPlainText(), '50'); expect(log.last.style!.fontSize, 14.0); expect(log.last.style!.color, theme.colorScheme.onInverseSurface); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Can update renderObject when secondaryTrackValue is updated', ( WidgetTester tester, ) async { final log = []; final loggingTrackShape = LoggingRoundedRectSliderTrackShape(secondaryOffsetLog: log); final theme = ThemeData(sliderTheme: SliderThemeData(trackShape: loggingTrackShape)); Widget buildSlider(double? secondaryTrackValue) { return MaterialApp( theme: theme, home: Material( child: Center( child: Slider( value: 0, secondaryTrackValue: secondaryTrackValue, onChanged: (double value) {}, ), ), ), ); } await tester.pumpWidget(buildSlider(null)); await tester.pumpAndSettle(); expect(log.last, isNull); await tester.pumpWidget(buildSlider(0.2)); await tester.pumpAndSettle(); expect(log.last, const Offset(174.4, 300.0)); await tester.pumpWidget(buildSlider(0.5)); await tester.pumpAndSettle(); expect(log.last, const Offset(400.0, 300.0)); }); // Regression test for hhttps://github.com/flutter/flutter/issues/161805 testWidgets('Discrete Slider 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, trackShape: RectangularSliderTrackShape(), ), ); Widget buildSlider({required double value}) { return MaterialApp( theme: theme, home: Material( child: SizedBox( width: 300, child: Slider(value: value, max: 100, divisions: 100, onChanged: (double value) {}), ), ), ); } await tester.pumpWidget(buildSlider(value: 0)); MaterialInkController material = Material.of(tester.element(find.byType(Slider))); expect(material, paints..circle(x: sliderPadding, y: 300.0, color: theme.colorScheme.primary)); await tester.pumpWidget(buildSlider(value: 100)); await tester.pumpAndSettle(); material = Material.of(tester.element(find.byType(Slider))); expect( material, paints..circle(x: 800.0 - sliderPadding, y: 300.0, color: theme.colorScheme.primary), ); }); testWidgets('Slider does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: Center( child: SizedBox.shrink(child: Slider(value: 1, onChanged: (_) {})), ), ), ), ); expect(tester.getSize(find.byType(Slider)), Size.zero); }); }