// 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/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; const TextStyle testStyle = TextStyle(fontSize: 10.0, letterSpacing: 0.0); void main() { testWidgets('Default layout minimum size', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const CupertinoButton(onPressed: null, child: Text('X', style: testStyle)), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect( buttonBox.size, // 1 10px character + 20px * 2 = 50.0 const Size(50.0, 44.0), ); }); testWidgets('Minimum size parameter', (WidgetTester tester) async { const minSize = 60.0; await tester.pumpWidget( boilerplate( child: const CupertinoButton( onPressed: null, minSize: minSize, child: Text('X', style: testStyle), ), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect( buttonBox.size, // 1 10px character + 20px * 2 = 50.0 (is smaller than minSize: 60.0) const Size.square(minSize), ); }); testWidgets('Minimum size minimumSize parameter', (WidgetTester tester) async { const size = Size(60.0, 100.0); await tester.pumpWidget( boilerplate( child: const CupertinoButton(onPressed: null, minimumSize: size, child: SizedBox.shrink()), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect(buttonBox.size, size); }); testWidgets('Size grows with text', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const CupertinoButton(onPressed: null, child: Text('XXXX', style: testStyle)), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect( buttonBox.size.width, // 4 10px character + 20px * 2 = 80.0 80.0, ); }); testWidgets('OnLongPress works!', (WidgetTester tester) async { var value = false; await tester.pumpWidget( boilerplate( child: CupertinoButton( onPressed: null, onLongPress: () { value = !value; }, child: const Text('XXXX', style: testStyle), ), ), ); await tester.pump(); final Finder cupertinoBtn = find.byType(CupertinoButton); await tester.longPress(cupertinoBtn); expect(value, isTrue); }); testWidgets('button is disabled if onLongPress and onPressed are both null', ( WidgetTester tester, ) async { await tester.pumpWidget( boilerplate( child: const CupertinoButton(onPressed: null, child: Text('XXXX', style: testStyle)), ), ); expect(find.byType(CupertinoButton), findsOneWidget); final CupertinoButton button = tester.widget(find.byType(CupertinoButton)); expect(button.enabled, isFalse); }); // TODO(LongCatIsLoong): Uncomment once https://github.com/flutter/flutter/issues/44115 // is fixed. /* testWidgets( 'CupertinoButton.filled default color contrast meets guideline', (WidgetTester tester) async { // The native color combination systemBlue text over white background fails // to pass the color contrast guideline. //await tester.pumpWidget( // CupertinoTheme( // data: const CupertinoThemeData(), // child: Directionality( // textDirection: TextDirection.ltr, // child: CupertinoButton.filled( // child: const Text('Button'), // onPressed: () {}, // ), // ), // ), //); //await expectLater(tester, meetsGuideline(textContrastGuideline)); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: CupertinoPageScaffold( child: CupertinoButton.filled( child: const Text('Button'), onPressed: () {}, ), ), ), ); await expectLater(tester, meetsGuideline(textContrastGuideline)); }); */ testWidgets('Button child alignment', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoButton(onPressed: () {}, child: const Text('button')), ), ); Align align = tester.firstWidget( find.ancestor(of: find.text('button'), matching: find.byType(Align)), ); expect(align.alignment, Alignment.center); // default await tester.pumpWidget( CupertinoApp( home: CupertinoButton( alignment: Alignment.centerLeft, onPressed: () {}, child: const Text('button'), ), ), ); align = tester.firstWidget( find.ancestor(of: find.text('button'), matching: find.byType(Align)), ); expect(align.alignment, Alignment.centerLeft); }); testWidgets('Button size changes depending on size property', (WidgetTester tester) async { const Widget child = Text('X', style: testStyle); await tester.pumpWidget( boilerplate( child: const CupertinoButton( onPressed: null, sizeStyle: CupertinoButtonSize.small, child: child, ), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect(buttonBox.size, const Size(34.0, 28.0)); await tester.pumpWidget( boilerplate( child: const CupertinoButton( onPressed: null, sizeStyle: CupertinoButtonSize.medium, child: child, ), ), ); expect(buttonBox.size, const Size(40.0, 32.0)); await tester.pumpWidget( boilerplate(child: const CupertinoButton(onPressed: null, child: child)), ); expect(buttonBox.size, const Size(50.0, 44.0)); }); testWidgets('Custom padding', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: const CupertinoButton( onPressed: null, padding: EdgeInsets.all(100.0), child: Text('X', style: testStyle), ), ), ); final RenderBox buttonBox = tester.renderObject(find.byType(CupertinoButton)); expect(buttonBox.size, const Size.square(210.0)); }); testWidgets('Button takes taps', (WidgetTester tester) async { var value = false; await tester.pumpWidget( StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return boilerplate( child: CupertinoButton( child: const Text('Tap me'), onPressed: () { setState(() { value = true; }); }, ), ); }, ), ); expect(value, isFalse); // No animating by default. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); await tester.tap(find.byType(CupertinoButton)); expect(value, isTrue); // Animates. expect(SchedulerBinding.instance.transientCallbackCount, equals(1)); }); testWidgets("Disabled button doesn't animate", (WidgetTester tester) async { await tester.pumpWidget( boilerplate(child: const CupertinoButton(onPressed: null, child: Text('Tap me'))), ); expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); await tester.tap(find.byType(CupertinoButton)); // Still doesn't animate. expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); }); testWidgets('Enabled button animates', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: CupertinoButton(child: const Text('Tap me'), onPressed: () {}), ), ); await tester.tap(find.byType(CupertinoButton)); // Enter animation. await tester.pump(); FadeTransition transition = tester.firstWidget(find.byType(FadeTransition)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(0.403, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 100)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(0.400, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(0.650, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(0.894, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(0.988, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, moreOrLessEquals(1.0, epsilon: 0.001)); }); testWidgets('pressedOpacity defaults to 0.1', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: CupertinoButton(child: const Text('Tap me'), onPressed: () {}), ), ); // Keep a "down" gesture on the button final Offset center = tester.getCenter(find.byType(CupertinoButton)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); // Check opacity final FadeTransition opacity = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(FadeTransition)), ); expect(opacity.opacity.value, 0.4); // Finish gesture to release resources. await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('pressedOpacity parameter', (WidgetTester tester) async { const pressedOpacity = 0.5; await tester.pumpWidget( boilerplate( child: CupertinoButton( pressedOpacity: pressedOpacity, child: const Text('Tap me'), onPressed: () {}, ), ), ); // Keep a "down" gesture on the button final Offset center = tester.getCenter(find.byType(CupertinoButton)); final TestGesture gesture = await tester.startGesture(center); await tester.pumpAndSettle(); // Check opacity final FadeTransition opacity = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(FadeTransition)), ); expect(opacity.opacity.value, pressedOpacity); // Finish gesture to release resources. await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('Cupertino button is semantically a button', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( boilerplate( child: Center( child: CupertinoButton(onPressed: () {}, child: const Text('ABC')), ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics.rootChild( actions: SemanticsAction.tap.index | SemanticsAction.focus.index, label: 'ABC', flags: [SemanticsFlag.isButton, SemanticsFlag.isFocusable], ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('Can specify colors', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: CupertinoButton( color: const Color(0x000000FF), disabledColor: const Color(0x0000FF00), onPressed: () {}, child: const Text('Skeuomorph me'), ), ), ); var decoration = tester.widget(find.widgetWithText(DecoratedBox, 'Skeuomorph me')).decoration as ShapeDecoration; expect(decoration.color, const Color(0x000000FF)); await tester.pumpWidget( boilerplate( child: const CupertinoButton( color: Color(0x000000FF), disabledColor: Color(0x0000FF00), onPressed: null, child: Text('Skeuomorph me'), ), ), ); decoration = tester.widget(find.widgetWithText(DecoratedBox, 'Skeuomorph me')).decoration as ShapeDecoration; expect(decoration.color, const Color(0x0000FF00)); }); testWidgets('Can specify dynamic colors', (WidgetTester tester) async { const Color bgColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF123456), darkColor: Color(0xFF654321), ); const Color inactive = CupertinoDynamicColor.withBrightness( color: Color(0xFF111111), darkColor: Color(0xFF222222), ); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(platformBrightness: Brightness.dark), child: boilerplate( child: CupertinoButton( color: bgColor, disabledColor: inactive, onPressed: () {}, child: const Text('Skeuomorph me'), ), ), ), ); var decoration = tester.widget(find.widgetWithText(DecoratedBox, 'Skeuomorph me')).decoration as ShapeDecoration; expect(decoration.color!.value, 0xFF654321); await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), child: boilerplate( child: const CupertinoButton( color: bgColor, disabledColor: inactive, onPressed: null, child: Text('Skeuomorph me'), ), ), ), ); decoration = tester.widget(find.widgetWithText(DecoratedBox, 'Skeuomorph me')).decoration as ShapeDecoration; // Disabled color. expect(decoration.color!.value, 0xFF111111); }); testWidgets('Button respects themes', (WidgetTester tester) async { late TextStyle textStyle; await tester.pumpWidget( CupertinoApp( home: CupertinoButton( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, isSameColorAs(CupertinoColors.activeBlue)); await tester.pumpWidget( CupertinoApp( home: CupertinoButton.tinted( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, CupertinoColors.activeBlue); var decoration = tester .widget( find.descendant( of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox), ), ) .decoration as ShapeDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue.withOpacity(0.12))); await tester.pumpWidget( CupertinoApp( home: CupertinoButton.filled( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, isSameColorAs(CupertinoColors.white)); decoration = tester .widget( find.descendant( of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox), ), ) .decoration as ShapeDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue)); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: CupertinoButton( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: CupertinoButton.tinted( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); decoration = tester .widget( find.descendant( of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox), ), ) .decoration as ShapeDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.activeBlue.darkColor.withOpacity(0.26))); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: CupertinoButton.filled( onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); expect(textStyle.color, isSameColorAs(CupertinoColors.white)); decoration = tester .widget( find.descendant( of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox), ), ) .decoration as ShapeDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.systemBlue.darkColor)); await tester.pumpWidget( CupertinoApp( home: CupertinoButton.filled( color: CupertinoColors.systemRed, onPressed: () {}, child: Builder( builder: (BuildContext context) { textStyle = DefaultTextStyle.of(context).style; return const Placeholder(); }, ), ), ), ); decoration = tester .widget( find.descendant( of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox), ), ) .decoration as ShapeDecoration; expect(decoration.color, isSameColorAs(CupertinoColors.systemRed)); }); testWidgets("All CupertinoButton const maps keys' match the available style sizes", ( WidgetTester tester, ) async { for (final CupertinoButtonSize size in CupertinoButtonSize.values) { expect(kCupertinoButtonPadding[size], isNotNull); expect(kCupertinoButtonSizeBorderRadius[size], isNotNull); expect(kCupertinoButtonMinSize[size], isNotNull); } }); testWidgets('Hovering over Cupertino button updates cursor to clickable on Web', ( WidgetTester tester, ) async { await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoButton.filled(onPressed: () {}, child: const Text('Tap me')), ), ), ); 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 button = tester.getCenter(find.byType(CupertinoButton)); await gesture.moveTo(button); await tester.pumpAndSettle(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); testWidgets('Button can be focused and has default colors', (WidgetTester tester) async { final focusNode = FocusNode(debugLabel: 'Button'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; final defaultFocusBorder = BorderSide( color: HSLColor.fromColor(CupertinoColors.activeBlue.withOpacity(kCupertinoFocusColorOpacity)) .withLightness(kCupertinoFocusColorBrightness) .withSaturation(kCupertinoFocusColorSaturation) .toColor(), width: 3.5, strokeAlign: BorderSide.strokeAlignOutside, ); await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoButton( onPressed: () {}, focusNode: focusNode, autofocus: true, child: const Text('Tap me'), ), ), ), ); expect(focusNode.hasPrimaryFocus, isTrue); // The button has no border. expect( _findBorder( tester, find.descendant(of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox)), ), BorderSide.none, ); await tester.pump(); // When focused, the button has a light blue border outline by default. focusNode.requestFocus(); await tester.pumpAndSettle(); expect( _findBorder( tester, find.descendant(of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox)), ), defaultFocusBorder, ); }); testWidgets('Button configures focus color', (WidgetTester tester) async { final focusNode = FocusNode(debugLabel: 'Button'); addTearDown(focusNode.dispose); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; const Color focusColor = CupertinoColors.systemGreen; await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoButton( onPressed: () {}, focusNode: focusNode, autofocus: true, focusColor: focusColor, child: const Text('Tap me'), ), ), ), ); expect(focusNode.hasPrimaryFocus, isTrue); focusNode.requestFocus(); await tester.pump(); await tester.pumpAndSettle(); final BorderSide borderSide = _findBorder( tester, find.descendant(of: find.byType(CupertinoButton), matching: find.byType(DecoratedBox)), ); expect(borderSide.color, focusColor); }); testWidgets('CupertinoButton.onFocusChange callback', (WidgetTester tester) async { final focusNode = FocusNode(debugLabel: 'CupertinoButton'); addTearDown(focusNode.dispose); var focused = false; await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoButton( onPressed: () {}, focusNode: focusNode, onFocusChange: (bool value) { focused = value; }, child: const Text('Tap me'), ), ), ), ); focusNode.requestFocus(); await tester.pump(); expect(focused, isTrue); expect(focusNode.hasFocus, isTrue); focusNode.unfocus(); await tester.pump(); expect(focused, isFalse); expect(focusNode.hasFocus, isFalse); }); testWidgets('IconThemeData falls back to default value when the TextStyle has a null size', ( WidgetTester tester, ) async { const defaultIconTheme = IconThemeData(size: kCupertinoButtonDefaultIconSize); IconThemeData? actualIconTheme; // Large size. await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( textTheme: CupertinoTextThemeData(actionTextStyle: TextStyle()), ), home: Center( child: CupertinoButton( onPressed: () {}, child: Builder( builder: (BuildContext context) { actualIconTheme = IconTheme.of(context); return const Placeholder(); }, ), ), ), ), ); expect(actualIconTheme?.size, defaultIconTheme.size); // Small size. await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( textTheme: CupertinoTextThemeData(actionSmallTextStyle: TextStyle()), ), home: Center( child: CupertinoButton( onPressed: () {}, child: Builder( builder: (BuildContext context) { actualIconTheme = IconTheme.of(context); return const Placeholder(); }, ), ), ), ), ); }); testWidgets('Button can be activated by keyboard shortcuts', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; var value = true; await tester.pumpWidget( CupertinoApp( home: Center( child: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return CupertinoButton( onPressed: () { setState(() { value = !value; }); }, autofocus: true, child: const Text('Tap me'), ); }, ), ), ), ); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); // On web, buttons don't respond to the enter key. expect(value, kIsWeb ? isTrue : isFalse); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pump(); expect(value, isFalse); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pump(); expect(value, isTrue); }); testWidgets('Press and move on button and animation works', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( child: CupertinoButton(onPressed: () {}, child: const Text('Tap me')), ), ); final TestGesture gesture = await tester.startGesture( tester.getTopLeft(find.byType(CupertinoButton)), ); addTearDown(gesture.removePointer); // Check opacity. final FadeTransition opacity = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(FadeTransition)), ); await tester.pumpAndSettle(); expect(opacity.opacity.value, 0.4); final double moveDistance = CupertinoButton.tapMoveSlop(); await gesture.moveBy(Offset(0, -moveDistance + 1)); await tester.pumpAndSettle(); expect(opacity.opacity.value, 0.4); await gesture.moveBy(const Offset(0, -2)); await tester.pumpAndSettle(); expect(opacity.opacity.value, 1.0); await gesture.moveBy(const Offset(0, 1)); await tester.pumpAndSettle(); expect(opacity.opacity.value, 0.4); }, variant: TargetPlatformVariant.all()); testWidgets('Drag outside button within ListView does not leave the button pressed', ( WidgetTester tester, ) async { await tester.pumpWidget( boilerplate( child: ListView( children: [CupertinoButton(onPressed: () {}, child: const Text('Tap me'))], ), ), ); final FadeTransition opacity = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(FadeTransition)), ); final TestGesture gesture = await tester.createGesture(); addTearDown(gesture.removePointer); await gesture.down(tester.getTopLeft(find.byType(CupertinoButton))); await gesture.moveBy(const Offset(1, 1)); await gesture.moveBy(Offset(0, -CupertinoButton.tapMoveSlop() - 5)); await tester.pumpAndSettle(); expect(opacity.opacity.value, 1.0); }); testWidgets('onPressed trigger takes into account MoveSlop.', (WidgetTester tester) async { var value = false; await tester.pumpWidget( boilerplate( child: CupertinoButton( onPressed: () { value = true; }, child: const Text('Tap me'), ), ), ); TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(CupertinoButton))); await gesture.moveTo( tester.getBottomRight(find.byType(CupertinoButton)) + Offset(0, CupertinoButton.tapMoveSlop()), ); await gesture.up(); expect(value, isFalse); gesture = await tester.startGesture(tester.getTopLeft(find.byType(CupertinoButton))); await gesture.moveTo( tester.getBottomRight(find.byType(CupertinoButton)) + Offset(0, CupertinoButton.tapMoveSlop()), ); await gesture.moveBy(const Offset(0, -1)); await gesture.up(); expect(value, isTrue); }); testWidgets('Mouse cursor resolves in enabled/disabled/pressed/focused states', ( WidgetTester tester, ) async { final focusNode = FocusNode(debugLabel: 'Button'); tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; addTearDown(focusNode.dispose); Widget buildButton({required bool enabled, MouseCursor? cursor}) { return CupertinoApp( home: Center( child: CupertinoButton( focusNode: focusNode, onPressed: enabled ? () {} : null, mouseCursor: cursor, child: const Text('Tap Me'), ), ), ); } // Test default mouse cursor final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); addTearDown(gesture.removePointer); await tester.pumpWidget(buildButton(enabled: true, cursor: const _ButtonMouseCursor())); await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoButton))); await tester.pump(); await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton))); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); // Test disabled state mouse cursor await tester.pumpWidget(buildButton(enabled: false, cursor: const _ButtonMouseCursor())); await gesture.moveTo(tester.getCenter(find.byType(CupertinoButton))); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden, ); // Test focused state mouse cursor await tester.pumpWidget(buildButton(enabled: true, cursor: const _ButtonMouseCursor())); focusNode.requestFocus(); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy, ); focusNode.unfocus(); // Test pressed state mouse cursor await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.down(tester.getCenter(find.byType(CupertinoButton))); await tester.pump(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab, ); await gesture.up(); await gesture.removePointer(); }); testWidgets('CupertinoButton foregroundColor applies to its text', (WidgetTester tester) async { const customForegroundColor = Color(0xFF5500FF); await tester.pumpWidget( boilerplate( child: CupertinoButton( onPressed: () {}, foregroundColor: customForegroundColor, child: const Text('Button'), ), ), ); // Check that the text has the custom foreground color final RichText text = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(RichText)), ); expect(text.text.style?.color, customForegroundColor); }); testWidgets('CupertinoButton foregroundColor applies to its icon', (WidgetTester tester) async { const customForegroundColor = Color(0xFF5500FF); await tester.pumpWidget( boilerplate( child: CupertinoButton( onPressed: () {}, foregroundColor: customForegroundColor, child: const Icon(IconData(0xE000)), ), ), ); // Check that the icon has the custom foreground color final IconTheme iconTheme = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(IconTheme)), ); expect(iconTheme.data.color, customForegroundColor); }); testWidgets( "CupertinoButton uses the theme's primaryColor when foregroundColor is not specified", (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoButton(onPressed: () {}, child: const Text('Button')), ), ), ); // The default color should be the primary color from the theme final BuildContext context = tester.element(find.text('Button')); final Color primaryColor = CupertinoTheme.of(context).primaryColor; final RichText text = tester.widget(find.byType(RichText)); expect(text.text.style?.color, primaryColor); }, ); testWidgets('CupertinoButton.filled foregroundColor applies to its text', ( WidgetTester tester, ) async { const customForegroundColor = Color(0xFF5500FF); await tester.pumpWidget( boilerplate( child: CupertinoButton.filled( onPressed: () {}, foregroundColor: customForegroundColor, child: const Text('Button'), ), ), ); // Check that the text has the custom foreground color final RichText text = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(RichText)), ); expect(text.text.style?.color, customForegroundColor); }); testWidgets('CupertinoButton foregroundColor applies to its text when disabled', ( WidgetTester tester, ) async { const customForegroundColor = Color(0xFF5500FF); await tester.pumpWidget( boilerplate( child: const CupertinoButton( onPressed: null, // disabled button foregroundColor: customForegroundColor, child: Text('Button'), ), ), ); // Check that the text has the custom foreground color even when disabled final RichText text = tester.widget( find.descendant(of: find.byType(CupertinoButton), matching: find.byType(RichText)), ); expect(text.text.style?.color, customForegroundColor); }); testWidgets('CupertinoButton does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: SizedBox.shrink( child: CupertinoButton(child: const Text('X'), onPressed: () {}), ), ), ), ); expect(tester.getSize(find.byType(CupertinoButton)), Size.zero); }); } Widget boilerplate({required Widget child}) { return Directionality( textDirection: TextDirection.ltr, child: Center(child: child), ); } class _ButtonMouseCursor extends WidgetStateMouseCursor { const _ButtonMouseCursor(); @override MouseCursor resolve(Set states) { return const WidgetStateProperty.fromMap({ WidgetState.disabled: SystemMouseCursors.forbidden, WidgetState.pressed: SystemMouseCursors.grab, WidgetState.focused: SystemMouseCursors.copy, WidgetState.any: SystemMouseCursors.basic, }).resolve(states); } @override String get debugDescription => '_ButtonMouseCursor()'; } BorderSide _findBorder(WidgetTester tester, Finder finder) { final decoration = tester.widget(finder).decoration as ShapeDecoration; return (decoration.shape as RoundedSuperellipseBorder).side; }