| // 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/rendering_tester.dart'; |
| import '../widgets/semantics_tester.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: 'CupertinoSystemDisplay', |
| fontSize: 21.0, |
| fontWeight: FontWeight.w400, |
| letterSpacing: -0.6, |
| color: CupertinoColors.black, |
| ), |
| ); |
| }); |
| |
| testWidgets('Picker semantics', (WidgetTester tester) async { |
| final semantics = SemanticsTester(tester); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) {}, |
| children: List<Widget>.generate(13, (int index) { |
| return SizedBox(height: 50.0, width: 300.0, child: Text(index.toString())); |
| }), |
| ), |
| ), |
| ), |
| ); |
| expect( |
| semantics, |
| includesNodeWith( |
| value: '0', |
| increasedValue: '1', |
| actions: <SemanticsAction>[SemanticsAction.increase], |
| ), |
| ); |
| |
| final hourListController = |
| tester.widget<ListWheelScrollView>(find.byType(ListWheelScrollView)).controller! |
| as FixedExtentScrollController; |
| |
| hourListController.jumpToItem(11); |
| await tester.pumpAndSettle(); |
| expect( |
| semantics, |
| includesNodeWith( |
| value: '11', |
| increasedValue: '12', |
| decreasedValue: '10', |
| actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease], |
| ), |
| ); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('Picker semantics excludes current item with empty label', ( |
| WidgetTester tester, |
| ) async { |
| // When the current item has an empty label (e.g., wrapped with ExcludeSemantics), |
| // the picker should not set any value, increasedValue, decreasedValue, or actions. |
| final semantics = SemanticsTester(tester); |
| final controller = FixedExtentScrollController(initialItem: 1); |
| addTearDown(controller.dispose); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: SizedBox( |
| height: 300.0, |
| width: 300.0, |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 50.0, |
| onSelectedItemChanged: (_) {}, |
| children: const <Widget>[ |
| Text('0'), |
| // Item at index 1 is excluded from semantics (simulating a disabled item). |
| ExcludeSemantics(child: Text('1')), |
| Text('2'), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // When the current item (index 1) has an empty label due to ExcludeSemantics, |
| // the picker should not have any value or actions set. |
| expect(semantics, isNot(includesNodeWith(value: '1'))); |
| // Also verify that no increase/decrease actions are set for this item. |
| expect( |
| semantics, |
| isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.increase])), |
| ); |
| expect( |
| semantics, |
| isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.decrease])), |
| ); |
| |
| // Scroll to item 0 which has a valid label. |
| controller.jumpToItem(0); |
| await tester.pumpAndSettle(); |
| |
| // Now the picker should have value '0' but no increase action |
| // because the next item (1) has an empty label. |
| expect(semantics, includesNodeWith(value: '0')); |
| expect( |
| semantics, |
| isNot(includesNodeWith(value: '0', actions: <SemanticsAction>[SemanticsAction.increase])), |
| ); |
| |
| // Scroll to item 2 which has a valid label. |
| controller.jumpToItem(2); |
| await tester.pumpAndSettle(); |
| |
| // Now the picker should have value '2' but no decrease action |
| // because the previous item (1) has an empty label. |
| expect(semantics, includesNodeWith(value: '2')); |
| expect( |
| semantics, |
| isNot(includesNodeWith(value: '2', actions: <SemanticsAction>[SemanticsAction.decrease])), |
| ); |
| |
| semantics.dispose(); |
| }); |
| |
| 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 controller = FixedExtentScrollController(initialItem: 1); |
| addTearDown(controller.dispose); |
| 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), |
| offsetMoreOrLessEquals(const Offset(0.0, 170.0), epsilon: 0.5), |
| ); |
| 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..rsuperellipse(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..rsuperellipse(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..rsuperellipse(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..rsuperellipse())); |
| }); |
| |
| group('scroll', () { |
| testWidgets( |
| 'scrolling calls onSelectedItemChanged and triggers haptic feedback when scroll passes middle of item', |
| (WidgetTester tester) async { |
| final selectedItems = <int>[]; |
| final 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())), |
| ); |
| }), |
| ), |
| ), |
| ); |
| // Drag to almost the middle of the next item. |
| await tester.drag( |
| find.text('0'), |
| const Offset(0.0, -90.0), |
| warnIfMissed: false, |
| ); // has an IgnorePointer |
| // Expect that the item changed, but haptics were not triggered yet, |
| // since we are not in the middle of the item. |
| expect(selectedItems, <int>[1]); |
| expect(systemCalls, isEmpty); |
| |
| // Let the scroll settle and end up in the middle of the item. |
| await tester.pumpAndSettle(); |
| expect(systemCalls, hasLength(2)); |
| // Check that the haptic feedback and ticking sound were triggered. |
| expect( |
| systemCalls[0], |
| isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'), |
| ); |
| expect(systemCalls[1], isMethodCall('SystemSound.play', arguments: 'SystemSoundType.tick')); |
| |
| // Overscroll a little to pass the middle of the item. |
| await tester.drag( |
| find.text('0'), |
| const Offset(0.0, 110.0), |
| warnIfMissed: false, |
| ); // has an IgnorePointer |
| expect(selectedItems, <int>[1, 0]); |
| expect(systemCalls, hasLength(4)); |
| expect( |
| systemCalls[2], |
| isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'), |
| ); |
| expect(systemCalls[3], isMethodCall('SystemSound.play', arguments: 'SystemSoundType.tick')); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
| ); |
| |
| testWidgets('scrolling with new behavior calls onSelectedItemChanged only when scroll ends', ( |
| WidgetTester tester, |
| ) async { |
| final selectedItems = <int>[]; |
| final 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, |
| changeReportingBehavior: ChangeReportingBehavior.onScrollEnd, |
| 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())), |
| ); |
| }), |
| ), |
| ), |
| ); |
| |
| final Offset initialOffset = tester.getTopLeft(find.text('0')); |
| // Drag to almost the middle of the next item. |
| final TestGesture scrollGesture = await tester.startGesture(initialOffset); |
| // Item 0 is still closest to the center. No updates. |
| await scrollGesture.moveBy(const Offset(0.0, -49.0)); |
| expect(selectedItems.isEmpty, true); |
| |
| // Now item 1 is closest to the center. |
| await scrollGesture.moveBy(const Offset(0.0, -1.0)); |
| expect(selectedItems, <int>[]); |
| |
| // Now item 1 is still closest to the center for another full itemExtent (100px). |
| await scrollGesture.moveBy(const Offset(0.0, -99.0)); |
| expect(selectedItems, <int>[]); |
| |
| await scrollGesture.moveBy(const Offset(0.0, -1.0)); |
| await scrollGesture.up(); |
| await tester.pumpAndSettle(); |
| expect(selectedItems, <int>[2]); |
| |
| await scrollGesture.down(initialOffset); |
| await scrollGesture.moveBy(const Offset(0.0, 100.0)); |
| expect(selectedItems, <int>[2]); |
| |
| await scrollGesture.up(); |
| expect(selectedItems, <int>[2, 1]); |
| }); |
| |
| testWidgets( |
| 'does not trigger haptics or sounds when scrolling by tapping on the item', |
| (WidgetTester tester) async { |
| final selectedItems = <int>[]; |
| final 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.tap(find.text('2'), warnIfMissed: false); // has an IgnorePointer |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| |
| // Expect that the item changed, but haptics were not triggered. |
| expect(selectedItems, <int>[1, 2]); |
| expect(systemCalls, isEmpty); |
| |
| await tester.drag(find.text('2'), const Offset(0.0, -30.0), warnIfMissed: false); |
| await tester.pumpAndSettle(const Duration(milliseconds: 10)); |
| // Expect that moving within the item does not trigger haptics after animating scroll. |
| expect(selectedItems, <int>[1, 2]); |
| expect(systemCalls, isEmpty); |
| }, |
| variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
| ); |
| |
| testWidgets( |
| 'do not trigger haptic or sounds on non-iOS devices', |
| (WidgetTester tester) async { |
| final selectedItems = <int>[]; |
| final 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 |
| |
| // Allow the scroll to settle in the middle of the item. |
| await tester.pumpAndSettle(); |
| |
| 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 controller = FixedExtentScrollController(initialItem: 10); |
| addTearDown(controller.dispose); |
| final 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), |
| pointer: 1, |
| 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, 250.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), |
| pointer: 1, |
| 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(340.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 controller = FixedExtentScrollController(initialItem: 10); |
| addTearDown(controller.dispose); |
| final 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, |
| }), |
| ); |
| }); |
| |
| // TODO(justinmc): Don't test Material interactions in Cupertino tests. |
| // https://github.com/flutter/flutter/issues/177028 |
| 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 ShapeDecoration?)?.shape as RoundedSuperellipseBorder?) |
| ?.borderRadius; |
| |
| expect(margin, isA<EdgeInsetsDirectional>()); |
| expect(borderRadius, isA<BorderRadiusDirectional>()); |
| }); |
| }); |
| |
| testWidgets('Scroll controller is detached upon dispose', (WidgetTester tester) async { |
| final controller = SpyFixedExtentScrollController(); |
| addTearDown(controller.dispose); |
| 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); |
| }); |
| |
| testWidgets('Registers taps and does not crash with certain diameterRatio', ( |
| WidgetTester tester, |
| ) async { |
| // Regression test for https://github.com/flutter/flutter/issues/126491 |
| |
| final children = List<int>.generate(100, (int index) => index); |
| final paintedChildren = <int>[]; |
| final tappedChildren = <int>{}; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: Center( |
| child: SizedBox( |
| height: 120, |
| child: CupertinoPicker( |
| itemExtent: 55, |
| diameterRatio: 0.9, |
| onSelectedItemChanged: (int index) {}, |
| children: children |
| .map<Widget>( |
| (int index) => GestureDetector( |
| key: ValueKey<int>(index), |
| onTap: () { |
| tappedChildren.add(index); |
| }, |
| child: SizedBox( |
| width: 55, |
| height: 55, |
| child: CustomPaint( |
| painter: TestCallbackPainter( |
| onPaint: () { |
| paintedChildren.add(index); |
| }, |
| ), |
| ), |
| ), |
| ), |
| ) |
| .toList(), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Children are painted two times for whatever reason |
| expect(paintedChildren, <int>[0, 1, 0, 1]); |
| |
| // Expect hitting 0 and 1, which are painted |
| await tester.tap(find.byKey(const ValueKey<int>(0))); |
| expect(tappedChildren, const <int>[0]); |
| |
| await tester.tap(find.byKey(const ValueKey<int>(1))); |
| expect(tappedChildren, const <int>[0, 1]); |
| |
| // The third child is not painted, so is not hit |
| await tester.tap(find.byKey(const ValueKey<int>(2)), warnIfMissed: false); |
| expect(tappedChildren, const <int>[0, 1]); |
| }); |
| |
| testWidgets('Tapping on child in a CupertinoPicker selects that child', ( |
| WidgetTester tester, |
| ) async { |
| var selectedItem = 0; |
| const tapScrollDuration = Duration(milliseconds: 300); |
| // The tap animation is set to 300ms, but add an extra 1µs to complete the scroll animation. |
| const infinitesimalPause = Duration(microseconds: 1); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPicker( |
| itemExtent: 10.0, |
| onSelectedItemChanged: (int i) { |
| selectedItem = i; |
| }, |
| children: const <Widget>[Text('0'), Text('1'), Text('2'), Text('3')], |
| ), |
| ), |
| ); |
| |
| expect(selectedItem, equals(0)); |
| // Tap on the item at index 1. |
| await tester.tap(find.text('1')); |
| await tester.pump(); |
| await tester.pump(tapScrollDuration + infinitesimalPause); |
| expect(selectedItem, equals(1)); |
| |
| // Skip to the item at index 3. |
| await tester.tap(find.text('3')); |
| await tester.pump(); |
| await tester.pump(tapScrollDuration + infinitesimalPause); |
| expect(selectedItem, equals(3)); |
| |
| // Tap on the item at index 0. |
| await tester.tap(find.text('0')); |
| await tester.pump(); |
| await tester.pump(tapScrollDuration + infinitesimalPause); |
| expect(selectedItem, equals(0)); |
| |
| // Skip to the item at index 2. |
| await tester.tap(find.text('2')); |
| await tester.pump(); |
| await tester.pump(tapScrollDuration + infinitesimalPause); |
| expect(selectedItem, equals(2)); |
| }); |
| |
| testWidgets('CupertinoPickerDefaultSelectionOverlay does not crash at zero area', ( |
| WidgetTester tester, |
| ) async { |
| tester.view.physicalSize = Size.zero; |
| addTearDown(tester.view.reset); |
| await tester.pumpWidget( |
| const CupertinoApp(home: Center(child: CupertinoPickerDefaultSelectionOverlay())), |
| ); |
| expect(tester.getSize(find.byType(CupertinoPickerDefaultSelectionOverlay)), Size.zero); |
| }); |
| |
| testWidgets('CupertinoPicker does not crash at zero area', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox.shrink( |
| child: CupertinoPicker( |
| itemExtent: 2.0, |
| onSelectedItemChanged: (_) {}, |
| children: const <Widget>[Text('X'), Text('Y')], |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(CupertinoPicker)), Size.zero); |
| }); |
| } |