| // 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/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import '../rendering/mock_canvas.dart'; |
| |
| class SpyFixedExtentScrollController extends FixedExtentScrollController { |
| /// Override for test visibility only. |
| @override |
| bool get hasListeners => super.hasListeners; |
| } |
| |
| void main() { |
| testWidgets('Picker respects theme styling', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) { }, |
| children: List<Widget>.generate(3, (int index) { |
| return SizedBox( |
| height: 50.0, |
| width: 300.0, |
| child: Text(index.toString()), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderParagraph paragraph = tester.renderObject(find.text('1')); |
| |
| expect(paragraph.text.style!.color, isSameColorAs(CupertinoColors.black)); |
| expect(paragraph.text.style!.copyWith(color: CupertinoColors.black), const TextStyle( |
| inherit: false, |
| fontFamily: '.SF Pro Display', |
| fontSize: 21.0, |
| fontWeight: FontWeight.w400, |
| letterSpacing: -0.6, |
| color: CupertinoColors.black, |
| )); |
| }); |
| |
| group('layout', () { |
| // Regression test for https://github.com/flutter/flutter/issues/22999 |
| testWidgets('CupertinoPicker.builder test', (WidgetTester tester) async { |
| Widget buildFrame(int childCount) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker.builder( |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) { }, |
| itemBuilder: (BuildContext context, int index) { |
| return Text('$index'); |
| }, |
| childCount: childCount, |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame(1)); |
| expect(tester.renderObject(find.text('0')).attached, true); |
| |
| await tester.pumpWidget(buildFrame(2)); |
| expect(tester.renderObject(find.text('0')).attached, true); |
| expect(tester.renderObject(find.text('1')).attached, true); |
| }); |
| |
| testWidgets('selected item is in the middle', (WidgetTester tester) async { |
| final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 1); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) { }, |
| children: List<Widget>.generate(3, (int index) { |
| return SizedBox( |
| height: 50.0, |
| width: 300.0, |
| child: Text(index.toString()), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '1').first), |
| const Offset(0.0, 125.0), |
| ); |
| |
| controller.jumpToItem(0); |
| await tester.pump(); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '1').first), |
| const Offset(0.0, 175.0), |
| ); |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0').first), |
| const Offset(0.0, 125.0), |
| ); |
| }); |
| }); |
| |
| testWidgets('picker dark mode', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData(brightness: Brightness.light), |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| backgroundColor: const CupertinoDynamicColor.withBrightness( |
| color: Color(0xFF123456), // Set alpha channel to FF to disable under magnifier painting. |
| darkColor: Color(0xFF654321), |
| ), |
| itemExtent: 15.0, |
| children: const <Widget>[Text('1'), Text('1')], |
| onSelectedItemChanged: (int i) { }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), paints..rrect(color: const Color.fromARGB(30, 118, 118, 128))); |
| expect(find.byType(CupertinoPicker), paints..rect(color: const Color(0xFF123456))); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData(brightness: Brightness.dark), |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| backgroundColor: const CupertinoDynamicColor.withBrightness( |
| color: Color(0xFF123456), |
| darkColor: Color(0xFF654321), |
| ), |
| itemExtent: 15.0, |
| children: const <Widget>[Text('1'), Text('1')], |
| onSelectedItemChanged: (int i) { }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), paints..rrect(color: const Color.fromARGB(61,118, 118, 128))); |
| expect(find.byType(CupertinoPicker), paints..rect(color: const Color(0xFF654321))); |
| }); |
| |
| testWidgets('picker selectionOverlay', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData(brightness: Brightness.light), |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| itemExtent: 15.0, |
| onSelectedItemChanged: (int i) {}, |
| selectionOverlay: const CupertinoPickerDefaultSelectionOverlay(background: Color(0x12345678)), |
| children: const <Widget>[Text('1'), Text('1')], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), paints..rrect(color: const Color(0x12345678))); |
| }); |
| |
| testWidgets('CupertinoPicker.selectionOverlay is nullable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData(brightness: Brightness.light), |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| itemExtent: 15.0, |
| onSelectedItemChanged: (int i) {}, |
| selectionOverlay: null, |
| children: const <Widget>[Text('1'), Text('1')], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), isNot(paints..rrect())); |
| }); |
| |
| group('scroll', () { |
| testWidgets( |
| 'scrolling calls onSelectedItemChanged and triggers haptic feedback', |
| (WidgetTester tester) async { |
| final List<int> selectedItems = <int>[]; |
| final List<MethodCall> systemCalls = <MethodCall>[]; |
| |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| systemCalls.add(methodCall); |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker( |
| itemExtent: 100.0, |
| onSelectedItemChanged: (int index) { selectedItems.add(index); }, |
| children: List<Widget>.generate(100, (int index) { |
| return Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 100.0, |
| child: Text(index.toString()), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, -100.0), warnIfMissed: false); // has an IgnorePointer |
| expect(selectedItems, <int>[1]); |
| expect( |
| systemCalls.single, |
| isMethodCall( |
| 'HapticFeedback.vibrate', |
| arguments: 'HapticFeedbackType.selectionClick', |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, 100.0), warnIfMissed: false); // has an IgnorePointer |
| expect(selectedItems, <int>[1, 0]); |
| expect(systemCalls, hasLength(2)); |
| expect( |
| systemCalls.last, |
| isMethodCall( |
| 'HapticFeedback.vibrate', |
| arguments: 'HapticFeedbackType.selectionClick', |
| ), |
| ); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
| ); |
| |
| testWidgets( |
| 'do not trigger haptic effects on non-iOS devices', |
| (WidgetTester tester) async { |
| final List<int> selectedItems = <int>[]; |
| final List<MethodCall> systemCalls = <MethodCall>[]; |
| |
| tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
| systemCalls.add(methodCall); |
| return null; |
| }); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker( |
| itemExtent: 100.0, |
| onSelectedItemChanged: (int index) { selectedItems.add(index); }, |
| children: List<Widget>.generate(100, (int index) { |
| return Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 100.0, |
| child: Text(index.toString()), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('0'), const Offset(0.0, -100.0), warnIfMissed: false); // has an IgnorePointer |
| expect(selectedItems, <int>[1]); |
| expect(systemCalls, isEmpty); |
| }, |
| variant: TargetPlatformVariant(TargetPlatform.values.where((TargetPlatform platform) => platform != TargetPlatform.iOS).toSet()), |
| ); |
| |
| testWidgets('a drag in between items settles back', (WidgetTester tester) async { |
| final FixedExtentScrollController controller = FixedExtentScrollController(initialItem: 10); |
| final List<int> selectedItems = <int>[]; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 100.0, |
| onSelectedItemChanged: (int index) { selectedItems.add(index); }, |
| children: List<Widget>.generate(100, (int index) { |
| return Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 100.0, |
| child: Text(index.toString()), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // Drag it by a bit but not enough to move to the next item. |
| await tester.drag(find.text('10'), const Offset(0.0, 30.0), touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer |
| |
| // The item that was in the center now moved a bit. |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '10')), |
| const Offset(200.0, 280.0), |
| ); |
| |
| await tester.pumpAndSettle(); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '10')).dy, |
| moreOrLessEquals(250.0, epsilon: 0.5), |
| ); |
| expect(selectedItems.isEmpty, true); |
| |
| // Drag it by enough to move to the next item. |
| await tester.drag(find.text('10'), const Offset(0.0, 70.0), touchSlopY: 0.0, warnIfMissed: false); // has an IgnorePointer |
| |
| await tester.pumpAndSettle(); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '10')).dy, |
| // It's down by 100.0 now. |
| moreOrLessEquals(350.0, epsilon: 0.5), |
| ); |
| expect(selectedItems, <int>[9]); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| |
| testWidgets('a big fling that overscrolls springs back', (WidgetTester tester) async { |
| final FixedExtentScrollController controller = |
| FixedExtentScrollController(initialItem: 10); |
| final List<int> selectedItems = <int>[]; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 100.0, |
| onSelectedItemChanged: (int index) { selectedItems.add(index); }, |
| children: List<Widget>.generate(100, (int index) { |
| return Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 100.0, |
| child: Text(index.toString()), |
| ), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| // A wild throw appears. |
| await tester.fling( |
| find.text('10'), |
| const Offset(0.0, 10000.0), |
| 1000.0, |
| warnIfMissed: false, // has an IgnorePointer |
| ); |
| |
| if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { |
| // Should have been flung far enough that even the first item goes off |
| // screen and gets removed. |
| expect(find.widgetWithText(SizedBox, '0').evaluate().isEmpty, true); |
| } |
| |
| expect( |
| selectedItems, |
| // This specific throw was fast enough that each scroll update landed |
| // on every second item. |
| <int>[8, 6, 4, 2, 0], |
| ); |
| |
| // Let it spring back. |
| await tester.pumpAndSettle(); |
| |
| expect( |
| tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, |
| // Should have sprung back to the middle now. |
| moreOrLessEquals(250.0), |
| ); |
| expect( |
| selectedItems, |
| // Falling back to 0 shouldn't produce more callbacks. |
| <int>[8, 6, 4, 2, 0], |
| ); |
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
| }); |
| |
| testWidgets('Picker adapts to MaterialApp dark mode', (WidgetTester tester) async { |
| Widget buildCupertinoPicker(Brightness brightness) { |
| return MaterialApp( |
| theme: ThemeData(brightness: brightness), |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) { }, |
| children: List<Widget>.generate(3, (int index) { |
| return SizedBox( |
| height: 50.0, |
| width: 300.0, |
| child: Text(index.toString()), |
| ); |
| }), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| // CupertinoPicker with light theme. |
| await tester.pumpWidget(buildCupertinoPicker(Brightness.light)); |
| RenderParagraph paragraph = tester.renderObject(find.text('1')); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| |
| // CupertinoPicker with dark theme. |
| await tester.pumpWidget(buildCupertinoPicker(Brightness.dark)); |
| paragraph = tester.renderObject(find.text('1')); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| }); |
| |
| group('CupertinoPickerDefaultSelectionOverlay', () { |
| testWidgets('should be using directional decoration', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData(brightness: Brightness.light), |
| home: CupertinoPicker( |
| itemExtent: 15.0, |
| onSelectedItemChanged: (int i) {}, |
| selectionOverlay: const CupertinoPickerDefaultSelectionOverlay(background: Color(0x12345678)), |
| children: const <Widget>[Text('1'), Text('1')], |
| ), |
| ), |
| ); |
| |
| final Finder selectionContainer = find.byType(Container); |
| final Container container = tester.firstWidget<Container>(selectionContainer); |
| final EdgeInsetsGeometry? margin = container.margin; |
| final BorderRadiusGeometry? borderRadius = (container.decoration as BoxDecoration?)?.borderRadius; |
| |
| expect(margin, isA<EdgeInsetsDirectional>()); |
| expect(borderRadius, isA<BorderRadiusDirectional>()); |
| }); |
| }); |
| |
| testWidgets('Scroll controller is detached upon dispose', (WidgetTester tester) async { |
| final SpyFixedExtentScrollController controller = SpyFixedExtentScrollController(); |
| expect(controller.hasListeners, false); |
| expect(controller.positions.length, 0); |
| |
| await tester.pumpWidget(CupertinoApp( |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: Center( |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) { }, |
| children: List<Widget>.generate(3, (int index) { |
| return SizedBox( |
| width: 300.0, |
| child: Text(index.toString()), |
| ); |
| }), |
| ), |
| ), |
| ), |
| )); |
| expect(controller.hasListeners, true); |
| expect(controller.positions.length, 1); |
| |
| await tester.pumpWidget(const SizedBox.expand()); |
| expect(controller.hasListeners, false); |
| expect(controller.positions.length, 0); |
| }); |
| |
| } |