// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; const CupertinoDynamicColor _kSystemFill = CupertinoDynamicColor( color: Color.fromARGB(51, 120, 120, 128), darkColor: Color.fromARGB(91, 120, 120, 128), highContrastColor: Color.fromARGB(71, 120, 120, 128), darkHighContrastColor: Color.fromARGB(112, 120, 120, 128), elevatedColor: Color.fromARGB(51, 120, 120, 128), darkElevatedColor: Color.fromARGB(91, 120, 120, 128), highContrastElevatedColor: Color.fromARGB(71, 120, 120, 128), darkHighContrastElevatedColor: Color.fromARGB(112, 120, 120, 128), ); void main() { Future<void> dragSlider(WidgetTester tester, Key sliderKey) { final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); const double unit = CupertinoThumbPainter.radius; const double delta = 3.0 * unit; return tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0)); } testWidgets('Slider does not move when tapped (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey), warnIfMissed: false); expect(value, equals(0.0)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider does not move when tapped (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); await tester.tap(find.byKey(sliderKey), warnIfMissed: false); expect(value, equals(0.0)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider calls onChangeStart once when interaction begins', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; int numberOfTimesOnChangeStartIsCalled = 0; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeStart: (double value) { numberOfTimesOnChangeStartIsCalled++; }, ), ), ); }, ), ), ), ); await dragSlider(tester, sliderKey); expect(numberOfTimesOnChangeStartIsCalled, equals(1)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider calls onChangeEnd once after interaction has ended', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; int numberOfTimesOnChangeEndIsCalled = 0; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeEnd: (double value) { numberOfTimesOnChangeEndIsCalled++; }, ), ), ); }, ), ), ), ); await dragSlider(tester, sliderKey); expect(numberOfTimesOnChangeEndIsCalled, equals(1)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider moves when dragged (LTR)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; late double startValue; late double endValue; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeStart: (double value) { startValue = value; }, onChangeEnd: (double value) { endValue = value; }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); const double unit = CupertinoThumbPainter.radius; const double delta = 3.0 * unit; await tester.dragFrom(topLeft + const Offset(unit, unit), const Offset(delta, 0.0)); final Size size = tester.getSize(find.byKey(sliderKey)); final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius)); expect(startValue, equals(0.0)); expect(value, equals(finalValue)); expect(endValue, equals(finalValue)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider moves when dragged (RTL)', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; late double startValue; late double endValue; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.rtl, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, onChangeStart: (double value) { setState(() { startValue = value; }); }, onChangeEnd: (double value) { setState(() { endValue = value; }); }, ), ), ); }, ), ), ), ); expect(value, equals(0.0)); final Offset bottomRight = tester.getBottomRight(find.byKey(sliderKey)); const double unit = CupertinoThumbPainter.radius; const double delta = 3.0 * unit; await tester.dragFrom(bottomRight - const Offset(unit, unit), const Offset(-delta, 0.0)); final Size size = tester.getSize(find.byKey(sliderKey)); final double finalValue = delta / (size.width - 2.0 * (8.0 + CupertinoThumbPainter.radius)); expect(startValue, equals(0.0)); expect(value, equals(finalValue)); expect(endValue, equals(finalValue)); await tester.pump(); // No animation should start. // Check the transientCallbackCount before tearing down the widget to ensure // that no animation is running. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Slider Semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), child: Directionality( textDirection: TextDirection.ltr, child: CupertinoSlider( value: 0.5, onChanged: (double v) { }, ), ), ), ); expect(semantics, hasSemantics( TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 1, value: '50%', increasedValue: '60%', decreasedValue: '40%', textDirection: TextDirection.ltr, flags: <SemanticsFlag>[SemanticsFlag.isSlider], actions: SemanticsAction.decrease.index | SemanticsAction.increase.index, ), ], ), ignoreRect: true, ignoreTransform: true, )); // Disable slider await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), child: Directionality( textDirection: TextDirection.ltr, child: CupertinoSlider( value: 0.5, onChanged: null, ), ), ), ); expect(semantics, hasSemantics( TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, flags: <SemanticsFlag>[SemanticsFlag.isSlider], ), ], ), ignoreRect: true, ignoreTransform: true, )); semantics.dispose(); }); testWidgets('Slider Semantics can be updated', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); double value = 0.5; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: CupertinoSlider( value: value, onChanged: (double v) { }, ), ), ), ); expect(tester.getSemantics(find.byType(CupertinoSlider)), matchesSemantics( isSlider: true, hasIncreaseAction: true, hasDecreaseAction: true, value: '50%', increasedValue: '60%', decreasedValue: '40%', textDirection: TextDirection.ltr, )); value = 0.6; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: CupertinoSlider( value: value, onChanged: (double v) { }, ), ), ), ); expect(tester.getSemantics(find.byType(CupertinoSlider)), matchesSemantics( isSlider: true, hasIncreaseAction: true, hasDecreaseAction: true, value: '60%', increasedValue: '70%', decreasedValue: '50%', textDirection: TextDirection.ltr, )); handle.dispose(); }); testWidgets('Slider respects themes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoSlider( onChanged: (double value) { }, value: 0.5, ), ), ), ); expect( find.byType(CupertinoSlider), // First line it paints is blue. paints..rrect(color: CupertinoColors.systemBlue.color), ); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: Center( child: CupertinoSlider( onChanged: (double value) { }, value: 0.5, ), ), ), ); expect( find.byType(CupertinoSlider), paints..rrect(color: CupertinoColors.systemBlue.darkColor), ); }); testWidgets('Themes can be overridden', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: Center( child: CupertinoSlider( activeColor: CupertinoColors.activeGreen, onChanged: (double value) { }, value: 0.5, ), ), ), ); expect( find.byType(CupertinoSlider), paints..rrect(color: CupertinoColors.systemGreen.darkColor), ); }); testWidgets('Themes can be overridden by dynamic colors', (WidgetTester tester) async { const CupertinoDynamicColor activeColor = CupertinoDynamicColor( color: Color(0x00000001), darkColor: Color(0x00000002), elevatedColor: Color(0x00000003), highContrastColor: Color(0x00000004), darkElevatedColor: Color(0x00000005), darkHighContrastColor: Color(0x00000006), highContrastElevatedColor: Color(0x00000007), darkHighContrastElevatedColor: Color(0x00000008), ); Widget withTraits(Brightness brightness, CupertinoUserInterfaceLevelData level, bool highContrast) { return CupertinoTheme( data: CupertinoThemeData(brightness: brightness), child: CupertinoUserInterfaceLevel( data: level, child: MediaQuery( data: MediaQueryData(highContrast: highContrast), child: Center( child: CupertinoSlider( activeColor: activeColor, onChanged: (double value) { }, value: 0.5, ), ), ), ), ); } await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.base, false))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.color)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.base, false))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.elevated, false))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkElevatedColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.base, true))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkHighContrastColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.dark, CupertinoUserInterfaceLevelData.elevated, true))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.darkHighContrastElevatedColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.base, true))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.highContrastColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.elevated, false))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.elevatedColor)); await tester.pumpWidget(CupertinoApp(home: withTraits(Brightness.light, CupertinoUserInterfaceLevelData.elevated, true))); expect(find.byType(CupertinoSlider), paints..rrect(color: activeColor.highContrastElevatedColor)); }); testWidgets('track color is dynamic', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.light), home: Center( child: CupertinoSlider( activeColor: CupertinoColors.activeGreen, onChanged: (double value) { }, value: 0, ), ), ), ); expect( find.byType(CupertinoSlider), paints..rrect(color: _kSystemFill.color), ); expect( find.byType(CupertinoSlider), isNot(paints..rrect(color: _kSystemFill.darkColor)), ); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: Center( child: CupertinoSlider( activeColor: CupertinoColors.activeGreen, onChanged: (double value) { }, value: 0, ), ), ), ); expect( find.byType(CupertinoSlider), paints..rrect(color: _kSystemFill.darkColor), ); expect( find.byType(CupertinoSlider), isNot(paints..rrect(color: _kSystemFill.color)), ); }); testWidgets('Thumb color can be overridden', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoSlider( thumbColor: CupertinoColors.systemPurple, onChanged: (double value) { }, value: 0, ), ), ), ); expect( find.byType(CupertinoSlider), paints ..rrect() ..rrect() ..rrect() ..rrect() ..rrect() ..rrect(color: CupertinoColors.systemPurple.color), ); await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoSlider( thumbColor: CupertinoColors.activeOrange, onChanged: (double value) { }, value: 0, ), ), ), ); expect( find.byType(CupertinoSlider), paints ..rrect() ..rrect() ..rrect() ..rrect() ..rrect() ..rrect(color: CupertinoColors.activeOrange.color), ); }); testWidgets('Hovering over Cupertino slider thumb updates cursor to clickable on Web', (WidgetTester tester) async { final Key sliderKey = UniqueKey(); double value = 0.0; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Material( child: Center( child: CupertinoSlider( key: sliderKey, value: value, onChanged: (double newValue) { setState(() { value = newValue; }); }, ), ), ); }, ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: const Offset(10, 10)); await tester.pumpAndSettle(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); final Offset topLeft = tester.getTopLeft(find.byKey(sliderKey)); await gesture.moveTo(topLeft + const Offset(15, 0)); addTearDown(gesture.removePointer); await tester.pumpAndSettle(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); }