// 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), 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..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, 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), 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 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); }); }