| // 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 '../rendering/mock_canvas.dart'; |
| import '../widgets/semantics_tester.dart'; |
| |
| // A thumb shape that also logs its repaint center. |
| class LoggingThumbShape extends SliderComponentShape { |
| LoggingThumbShape(this.log); |
| |
| final List<Offset> log; |
| |
| @override |
| Size getPreferredSize(bool isEnabled, bool isDiscrete) { |
| return const Size(10.0, 10.0); |
| } |
| |
| @override |
| void paint( |
| PaintingContext context, |
| Offset thumbCenter, { |
| required Animation<double> activationAnimation, |
| required Animation<double> 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 Paint thumbPaint = Paint()..color = Colors.red; |
| context.canvas.drawCircle(thumbCenter, 5.0, thumbPaint); |
| } |
| } |
| |
| 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<double> enableAnimation, |
| required bool isEnabled, |
| required TextDirection textDirection, |
| }) { |
| final Paint paint = Paint()..color = Colors.red; |
| context.canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, 10.0, 20.0), paint); |
| } |
| } |
| |
| class _StateDependentMouseCursor extends MaterialStateMouseCursor { |
| const _StateDependentMouseCursor({ |
| this.disabled = SystemMouseCursors.none, |
| this.dragged = SystemMouseCursors.none, |
| this.hovered = SystemMouseCursors.none, |
| }); |
| |
| final MouseCursor disabled; |
| final MouseCursor hovered; |
| final MouseCursor dragged; |
| |
| @override |
| MouseCursor resolve(Set<MaterialState> states) { |
| if (states.contains(MaterialState.disabled)) { |
| return disabled; |
| } |
| if (states.contains(MaterialState.dragged)) { |
| return dragged; |
| } |
| if (states.contains(MaterialState.hovered)) { |
| return hovered; |
| } |
| return SystemMouseCursors.none; |
| } |
| |
| @override |
| String get debugDescription => '_StateDependentMouseCursor'; |
| } |
| |
| void main() { |
| testWidgets('The initial value should respect the discrete value', (WidgetTester tester) async { |
| final Key sliderKey = UniqueKey(); |
| double value = 0.20; |
| final List<Offset> log = <Offset>[]; |
| final LoggingThumbShape 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(212.0, 300.0)); |
| }); |
| |
| testWidgets('Slider can move when tapped (LTR)', (WidgetTester tester) async { |
| final Key sliderKey = UniqueKey(); |
| double value = 0.0; |
| double? startValue; |
| double? endValue; |
| |
| await tester.pumpWidget( |
| 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(); |
| double 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(); |
| double value = 0.0; |
| late double startValue; |
| late double endValue; |
| int updates = 0; |
| int startValueUpdates = 0; |
| int 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(); |
| double 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(); |
| double value = 0.0; |
| final List<Offset> log = <Offset>[]; |
| final LoggingThumbShape 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 List<Offset> expectedLog = <Offset>[ |
| const Offset(24.0, 300.0), |
| const Offset(24.0, 300.0), |
| const Offset(400.0, 300.0), |
| ]; |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(value, equals(0.5)); |
| expect(log.length, 3); |
| expect(log, orderedEquals(expectedLog)); |
| await gesture.moveBy(const Offset(-500.0, 0.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(value, equals(0.0)); |
| expect(log.length, 5); |
| expect(log.last.dx, 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.5, epsilon: 0.1)); |
| // Final position. |
| await tester.pump(const Duration(milliseconds: 80)); |
| expectedLog.add(const Offset(24.0, 300.0)); |
| expect(value, equals(0.0)); |
| expect(log.length, 8); |
| expect(log.last.dx, moreOrLessEquals(24.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(); |
| double value = 0.0; |
| int 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(); |
| double value = 0.0; |
| final List<Offset> log = <Offset>[]; |
| final LoggingThumbShape 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 List<Offset> expectedLog = <Offset>[ |
| const Offset(24.0, 300.0), |
| const Offset(24.0, 300.0), |
| const Offset(400.0, 300.0), |
| ]; |
| final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 100)); |
| expect(value, equals(0.5)); |
| expect(log.length, 3); |
| expect(log, orderedEquals(expectedLog)); |
| await gesture.moveBy(const Offset(-500.0, 0.0)); |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(value, equals(0.0)); |
| expect(log.length, 5); |
| expect(log.last.dx, 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.5, epsilon: 0.1)); |
| // Final position. |
| await tester.pump(const Duration(milliseconds: 80)); |
| expectedLog.add(const Offset(24.0, 300.0)); |
| expect(value, equals(0.0)); |
| expect(log.length, 8); |
| expect(log.last.dx, moreOrLessEquals(24.0, epsilon: 0.1)); |
| await gesture.up(); |
| }); |
| |
| testWidgets('Slider take on discrete values', (WidgetTester tester) async { |
| final Key sliderKey = UniqueKey(); |
| double value = 0.0; |
| |
| await tester.pumpWidget( |
| 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 List<double> log = <double>[]; |
| 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, <double>[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, <double>[]); |
| log.clear(); |
| }); |
| |
| testWidgets('Slider can tap in vertical scroller', (WidgetTester tester) async { |
| double value = 0.0; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: ListView( |
| children: <Widget>[ |
| Slider( |
| value: value, |
| onChanged: (double newValue) { |
| value = newValue; |
| }, |
| ), |
| Container( |
| height: 2000.0, |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.tap(find.byType(Slider)); |
| expect(value, equals(0.5)); |
| }); |
| |
| testWidgets('Slider drags immediately (LTR)', (WidgetTester tester) async { |
| double value = 0.0; |
| await tester.pumpWidget( |
| 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 { |
| double 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 |
| |
| int startFired = 0; |
| int 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<RenderBox>(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<RenderBox>(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<RenderBox>(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(); |
| double value = 0.0; |
| |
| Widget buildSlider({ |
| required double textScaleFactor, |
| bool isDiscrete = true, |
| ShowValueIndicator show = ShowValueIndicator.onlyForDiscrete, |
| }) { |
| return MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return MediaQuery( |
| data: MediaQueryData(textScaleFactor: textScaleFactor), |
| child: Material( |
| child: Theme( |
| data: Theme.of(context).copyWith( |
| sliderTheme: Theme.of(context).sliderTheme.copyWith(showValueIndicator: show), |
| ), |
| child: Center( |
| child: OverflowBox( |
| maxWidth: double.infinity, |
| maxHeight: double.infinity, |
| child: Slider( |
| key: sliderKey, |
| 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>[ |
| 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>[ |
| 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>[ |
| 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>[ |
| 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('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'); |
| double value = 0.0; |
| |
| Widget buildSlider(int parents) { |
| Widget createParents(int parents, StateSetter setState) { |
| Widget slider = Slider( |
| key: sliderKey, |
| value: value, |
| divisions: 4, |
| onChanged: (double newValue) { |
| setState(() { |
| value = newValue; |
| }); |
| }, |
| ); |
| |
| for (int i = 0; i < parents; ++i) { |
| slider = Column(children: <Widget>[slider]); |
| } |
| return slider; |
| } |
| |
| return MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Material( |
| child: createParents(parents, setState), |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> 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: 24.0, y: 24.0, radius: 10.0), |
| ); |
| |
| gesture = await tester.startGesture(center); |
| await tester.pump(); |
| // Wait for animations to start. |
| await tester.pump(const Duration(milliseconds: 25)); |
| expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); |
| expect( |
| material, |
| paints |
| ..circle(x: 111.20703125, 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: 111.20703125, y: 24.0, radius: 10.0), |
| ); |
| |
| // Reparenting in the middle of an animation should do nothing. |
| if (reparent) { |
| await tester.pumpWidget(buildSlider(2)); |
| } |
| |
| // Move a little further in the animations. |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(SchedulerBinding.instance.transientCallbackCount, equals(2)); |
| expect( |
| material, |
| paints |
| ..circle(x: 190.0135726928711, 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: 190.0135726928711, 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 SemanticsTester 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isSlider, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.increase, |
| SemanticsAction.decrease, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| // isFocusable is delayed by 1 frame. |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isSlider, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| await tester.pump(); |
| expect( |
| semantics, |
| hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isSlider, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| semantics.dispose(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux })); |
| |
| testWidgets('Slider Semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Theme( |
| data: ThemeData.light(), |
| 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider], |
| actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease], |
| value: '50%', |
| increasedValue: '60%', |
| decreasedValue: '40%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 5, |
| flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isSlider], |
| value: '50%', |
| increasedValue: '60%', |
| decreasedValue: '40%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| semantics.dispose(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Slider Semantics', (WidgetTester tester) async { |
| final SemanticsTester 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isSlider, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.increase, |
| SemanticsAction.decrease, |
| SemanticsAction.didGainAccessibilityFocus, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| // isFocusable is delayed by 1 frame. |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isSlider, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.didGainAccessibilityFocus, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| await tester.pump(); |
| expect( |
| semantics, |
| hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isSlider, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.didGainAccessibilityFocus, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| |
| semantics.dispose(); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })); |
| |
| testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async { |
| final SemanticsTester 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>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider], |
| actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease], |
| value: '40', |
| increasedValue: '60', |
| decreasedValue: '20', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/101868 |
| testWidgets('Slider.label info should not write to semantic node', (WidgetTester tester) async { |
| final SemanticsTester 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) { }, |
| label: 'Bingo', |
| ), |
| ), |
| ), |
| )); |
| |
| expect( |
| semantics, |
| hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider], |
| actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease], |
| value: '40', |
| increasedValue: '60', |
| decreasedValue: '20', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ignoreRect: true, |
| ignoreTransform: true, |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Slider is focusable and has correct focus color', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| double 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('Slider has correct focus color from overlayColor property', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| double 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: MaterialStateColor.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.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 ThemeData theme = ThemeData(useMaterial3: true); |
| double 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 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; |
| double 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: MaterialStateColor.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.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('Slider is draggable and has correct dragged color', (WidgetTester tester) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| double value = 0.5; |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final Key sliderKey = UniqueKey(); |
| |
| Widget buildApp({bool enabled = true}) { |
| return MaterialApp( |
| theme: theme, |
| home: Material( |
| child: Center( |
| child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return Slider( |
| value: value, |
| key: sliderKey, |
| 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 still has overlay when stopped dragging. |
| expect( |
| Material.of(tester.element(find.byType(Slider))), |
| paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), |
| ); |
| }); |
| |
| testWidgets('Slider has correct dragged color from overlayColor property', (WidgetTester tester) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| double value = 0.5; |
| final Key sliderKey = UniqueKey(); |
| |
| Widget buildApp({bool enabled = true}) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return Slider( |
| value: value, |
| key: sliderKey, |
| overlayColor: MaterialStateColor.resolveWith((Set<MaterialState> states) { |
| if (states.contains(MaterialState.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 still has overlay when stopped dragging. |
| expect( |
| Material.of(tester.element(find.byType(Slider))), |
| paints..circle(color: Colors.lime[500]), |
| ); |
| }); |
| |
| testWidgets('OverlayColor property is correctly applied when activeColor is also provided', (WidgetTester tester) async { |
| final FocusNode focusNode = FocusNode(debugLabel: 'Slider'); |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| double value = 0.5; |
| const Color activeColor = Color(0xffff0000); |
| const Color 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<Color?>(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; |
| double 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; |
| }); |
| }, |
| autofocus: true, |
| ); |
| }), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| expect(value, 0.55); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| expect(value, 0.55); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ 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; |
| double 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; |
| }); |
| }, |
| autofocus: true, |
| ); |
| }), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| expect(value, 0.6); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| expect(value, 0.6); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('Slider can be incremented and decremented by keyboard shortcuts - RTL', (WidgetTester tester) async { |
| tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
| double value = 0.5; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return Directionality( |
| textDirection: TextDirection.rtl, |
| child: Slider( |
| value: value, |
| onChanged: (double newValue) { |
| setState(() { |
| value = newValue; |
| }); |
| }, |
| autofocus: true, |
| ), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| expect(value, 0.45); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| expect(value, 0.55); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ 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; |
| double value = 0.5; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Center( |
| child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { |
| return Directionality( |
| textDirection: TextDirection.rtl, |
| child: Slider( |
| value: value, |
| onChanged: (double newValue) { |
| setState(() { |
| value = newValue; |
| }); |
| }, |
| autofocus: true, |
| ), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| expect(value, 0.4); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pumpAndSettle(); |
| expect(value, 0.6); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| expect(value, 0.5); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('In directional nav, Slider can be navigated out of by using up and down arrows', (WidgetTester tester) async { |
| const Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ |
| 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; |
| double topSliderValue = 0.5; |
| double 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: <Widget>[ |
| 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 SemanticsTester semantics = SemanticsTester(tester); |
| final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; |
| final FocusNode focusNode = FocusNode(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Material( |
| child: Slider( |
| value: 0.5, |
| onChanged: (double _) {}, |
| focusNode: focusNode, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(semantics, hasSemantics( |
| TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 1, |
| textDirection: TextDirection.ltr, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 2, |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 3, |
| flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], |
| children: <TestSemantics>[ |
| TestSemantics( |
| id: 4, |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.hasEnabledState, |
| SemanticsFlag.isEnabled, |
| SemanticsFlag.isFocusable, |
| SemanticsFlag.isSlider, |
| ], |
| actions: <SemanticsAction>[ |
| SemanticsAction.increase, |
| SemanticsAction.decrease, |
| SemanticsAction.didGainAccessibilityFocus, |
| ], |
| value: '50%', |
| increasedValue: '55%', |
| decreasedValue: '45%', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| ], |
| ), |
| 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>{ TargetPlatform.windows })); |
| |
| testWidgets('Value indicator appears when it should', (WidgetTester tester) async { |
| final ThemeData baseTheme = ThemeData( |
| platform: TargetPlatform.android, |
| primarySwatch: Colors.blue, |
| ); |
| SliderThemeData theme = baseTheme.sliderTheme; |
| double value = 0.45; |
| Widget buildApp({ required SliderThemeData sliderTheme, int? divisions, bool enabled = true }) { |
| final ValueChanged<double>? 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<void> expectValueIndicator({ |
| required bool isVisible, |
| required SliderThemeData theme, |
| int? divisions, |
| bool enabled = true, |
| }) async { |
| // Discrete enabled widget. |
| await tester.pumpWidget(buildApp(sliderTheme: theme, divisions: divisions, enabled: enabled)); |
| final Offset center = tester.getCenter(find.byType(Slider)); |
| final TestGesture gesture = await tester.startGesture(center); |
| // Wait for value indicator animation to finish. |
| await tester.pumpAndSettle(); |
| |
| |
| final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
| expect( |
| valueIndicatorBox, |
| isVisible |
| ? (paints..path(color: theme.valueIndicatorColor)..paragraph()) |
| : isNot(paints..path(color: theme.valueIndicatorColor)..paragraph()), |
| ); |
| 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); |
| |
| // 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); |
| |
| // discrete enabled widget with showValueIndicator set to always. |
| theme = theme.copyWith(showValueIndicator: ShowValueIndicator.always); |
| 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); |
| |
| // 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); |
| }); |
| |
| testWidgets("Slider doesn't start any animations after dispose", (WidgetTester tester) async { |
| final Key sliderKey = UniqueKey(); |
| double 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 Color fillColor = Color(0xf55f5f5f); |
| double value = 0.0; |
| |
| Widget buildApp({ |
| int? divisions, |
| bool enabled = true, |
| }) { |
| return MaterialApp( |
| 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: <Widget>[ |
| 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<void>( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| child: const Text('Inner page'), |
| onPressed: () { Navigator.of(context).pop(); }, |
| ); |
| }, |
| ), |
| ); |
| }, |
| ), |
| ], |
| ); |
| }, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildApp(divisions: 3)); |
| |
| final RenderObject valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
| final Offset topRight = tester.getTopRight(find.byType(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 { |
| double value = 0.5; |
| |
| Widget buildFrame(TargetPlatform platform) { |
| return MaterialApp( |
| theme: ThemeData(platform: platform), |
| home: StatefulBuilder( |
| builder: (BuildContext context, StateSetter setState) { |
| return Material( |
| child: Center( |
| child: Slider.adaptive( |
| value: value, |
| onChanged: (double newValue) { |
| setState(() { |
| value = newValue; |
| }); |
| }, |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| } |
| |
| for (final TargetPlatform platform in <TargetPlatform>[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 TargetPlatform platform in <TargetPlatform>[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(); |
| double 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<RenderBox>(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 MaterialStateMouseCursor resolves correctly', (WidgetTester tester) async { |
| const MouseCursor disabledCursor = SystemMouseCursors.basic; |
| const MouseCursor hoveredCursor = SystemMouseCursors.grab; |
| const MouseCursor draggedCursor = SystemMouseCursors.move; |
| |
| Widget buildFrame({ required bool enabled }) { |
| return MaterialApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: Material( |
| child: Center( |
| child: MouseRegion( |
| cursor: SystemMouseCursors.forbidden, |
| child: Slider( |
| mouseCursor: const _StateDependentMouseCursor( |
| disabled: disabledCursor, |
| hovered: hoveredCursor, |
| dragged: draggedCursor, |
| ), |
| value: 0.5, |
| onChanged: enabled ? (double newValue) { } : null, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); |
| await gesture.addPointer(location: Offset.zero); |
| |
| await tester.pumpWidget(buildFrame(enabled: false)); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor); |
| |
| await tester.pumpWidget(buildFrame(enabled: true)); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.none); |
| |
| await gesture.moveTo(tester.getCenter(find.byType(Slider))); // start hover |
| await tester.pumpAndSettle(); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor); |
| |
| await tester.timedDrag( |
| find.byType(Slider), |
| const Offset(20.0, 0.0), |
| const Duration(milliseconds: 100), |
| ); |
| expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.move); |
| }); |
| |
| testWidgets('Slider implements debugFillProperties', (WidgetTester tester) async { |
| final DiagnosticPropertiesBuilder 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<String> description = builder.properties |
| .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
| .map((DiagnosticsNode node) => node.toString()).toList(); |
| |
| expect(description, <String>[ |
| 'value: 50.0', |
| 'secondaryTrackValue: 75.0', |
| 'disabled', |
| 'min: 0.0', |
| 'max: 100.0', |
| 'divisions: 10', |
| 'label: "Set a value"', |
| 'activeColor: MaterialColor(primary value: Color(0xff2196f3))', |
| 'inactiveColor: MaterialColor(primary value: Color(0xff9e9e9e))', |
| 'secondaryActiveColor: MaterialColor(primary value: 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, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // _RenderSlider is the last render object in the tree. |
| final RenderObject renderObject = tester.allRenderObjects.last; |
| |
| // 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('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, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // _RenderSlider is the last render object in the tree. |
| final RenderObject renderObject = tester.allRenderObjects.last; |
| |
| expect( |
| renderObject, |
| paints |
| // active track RRect |
| ..rrect(rrect: RRect.fromLTRBAndCorners(-14.0, 2.0, 5.0, 8.0, topLeft: const Radius.circular(3.0), bottomLeft: const Radius.circular(3.0))) |
| // inactive track RRect |
| ..rrect(rrect: RRect.fromLTRBAndCorners(5.0, 3.0, 24.0, 7.0, topRight: const Radius.circular(2.0), bottomRight: const Radius.circular(2.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)); |
| |
| // _RenderSlider is the last render object in the tree. |
| final RenderObject renderObject = tester.allRenderObjects.last; |
| |
| // Update the divisions from 10 to 15, the thumb should be paint at the correct position. |
|