| // 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. |
| |
| // reduced-test-set: |
| // This file is run as part of a reduced test set in CI on Mac and Windows |
| // machines. |
| @Tags(<String>['reduced-test-set']) |
| library; |
| |
| import 'dart:math' as math; |
| import 'dart:ui'; |
| |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| // A number of the hit tests below say "warnIfMissed: false". This is because |
| // the way the CupertinoPicker works, the hits don't actually reach the labels, |
| // the scroll view intercepts them. |
| |
| // scrolling by this offset will move the picker to the next item |
| const Offset _kRowOffset = Offset(0.0, -50.0); |
| |
| void main() { |
| group('Countdown timer picker', () { |
| testWidgets('initialTimerDuration falls within limit', (WidgetTester tester) async { |
| expect(() { |
| CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(days: 1), |
| ); |
| }, throwsAssertionError); |
| |
| expect(() { |
| CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(seconds: -1), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('minuteInterval is positive and is a factor of 60', (WidgetTester tester) async { |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, minuteInterval: 0); |
| }, throwsAssertionError); |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, minuteInterval: -1); |
| }, throwsAssertionError); |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, minuteInterval: 7); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('secondInterval is positive and is a factor of 60', (WidgetTester tester) async { |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, secondInterval: 0); |
| }, throwsAssertionError); |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, secondInterval: -1); |
| }, throwsAssertionError); |
| expect(() { |
| CupertinoTimerPicker(onTimerDurationChanged: (_) {}, secondInterval: 7); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('background color default value', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp(home: CupertinoTimerPicker(onTimerDurationChanged: (_) {})), |
| ); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect(pickers.any((CupertinoPicker picker) => picker.backgroundColor != null), false); |
| }); |
| |
| testWidgets('background color can be null', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp(home: CupertinoTimerPicker(onTimerDurationChanged: (_) {})), |
| ); |
| |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('specified background color is applied', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| backgroundColor: CupertinoColors.black, |
| ), |
| ), |
| ); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect( |
| pickers.any((CupertinoPicker picker) => picker.backgroundColor != CupertinoColors.black), |
| false, |
| ); |
| }); |
| |
| testWidgets('specified item extent value is applied', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp(home: CupertinoTimerPicker(itemExtent: 42, onTimerDurationChanged: (_) {})), |
| ); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect(pickers.any((CupertinoPicker picker) => picker.itemExtent != 42), false); |
| }); |
| |
| testWidgets('columns are ordered correctly when text direction is ltr', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(hours: 12, minutes: 30, seconds: 59), |
| ), |
| ), |
| ); |
| |
| Offset lastOffset = tester.getTopLeft(find.text('12')); |
| |
| expect(tester.getTopLeft(find.text('hours')).dx > lastOffset.dx, true); |
| lastOffset = tester.getTopLeft(find.text('hours')); |
| |
| expect(tester.getTopLeft(find.text('30')).dx > lastOffset.dx, true); |
| lastOffset = tester.getTopLeft(find.text('30')); |
| |
| expect(tester.getTopLeft(find.text('min.')).dx > lastOffset.dx, true); |
| lastOffset = tester.getTopLeft(find.text('min.')); |
| |
| expect(tester.getTopLeft(find.text('59')).dx > lastOffset.dx, true); |
| lastOffset = tester.getTopLeft(find.text('59')); |
| |
| expect(tester.getTopLeft(find.text('sec.')).dx > lastOffset.dx, true); |
| }); |
| |
| testWidgets('columns are ordered correctly when text direction is rtl', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Directionality( |
| textDirection: TextDirection.rtl, |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(hours: 12, minutes: 30, seconds: 59), |
| ), |
| ), |
| ), |
| ); |
| |
| Offset lastOffset = tester.getTopLeft(find.text('12')); |
| |
| expect(tester.getTopLeft(find.text('hours')).dx > lastOffset.dx, false); |
| lastOffset = tester.getTopLeft(find.text('hours')); |
| |
| expect(tester.getTopLeft(find.text('30')).dx > lastOffset.dx, false); |
| lastOffset = tester.getTopLeft(find.text('30')); |
| |
| expect(tester.getTopLeft(find.text('min.')).dx > lastOffset.dx, false); |
| lastOffset = tester.getTopLeft(find.text('min.')); |
| |
| expect(tester.getTopLeft(find.text('59')).dx > lastOffset.dx, false); |
| lastOffset = tester.getTopLeft(find.text('59')); |
| |
| expect(tester.getTopLeft(find.text('sec.')).dx > lastOffset.dx, false); |
| }); |
| |
| testWidgets('width of picker is consistent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(hours: 12, minutes: 30, seconds: 59), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first column and the last column. |
| final double distance = |
| tester.getCenter(find.text('sec.')).dx - tester.getCenter(find.text('12')).dx; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: SizedBox( |
| height: 400.0, |
| width: 800.0, |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (_) {}, |
| initialTimerDuration: const Duration(hours: 12, minutes: 30, seconds: 59), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first and the last column should be the same. |
| expect( |
| tester.getCenter(find.text('sec.')).dx - tester.getCenter(find.text('12')).dx, |
| distance, |
| ); |
| }); |
| |
| testWidgets('onScrollEnd behavior reports changes correctly', (WidgetTester tester) async { |
| final selectedDurations = <Duration>[]; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoTimerPicker( |
| initialTimerDuration: const Duration(hours: 1, minutes: 30, seconds: 15), |
| changeReportingBehavior: ChangeReportingBehavior.onScrollEnd, |
| onTimerDurationChanged: (Duration duration) => selectedDurations.add(duration), |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Offset initialOffset = tester.getTopLeft(find.text('30')); |
| |
| final TestGesture scrollGesture = await tester.startGesture(initialOffset); |
| // Should not report changes until the gesture ends. |
| await scrollGesture.moveBy(const Offset(0.0, 32.0)); |
| expect(selectedDurations, isEmpty); |
| |
| await scrollGesture.moveBy(const Offset(0.0, 32.0)); |
| expect(selectedDurations, isEmpty); |
| |
| await scrollGesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Only reports the last change. |
| expect(selectedDurations, hasLength(1)); |
| expect(selectedDurations.first, const Duration(hours: 1, minutes: 28, seconds: 15)); |
| }); |
| }); |
| |
| testWidgets('showDayOfWeek is only supported in date mode', (WidgetTester tester) async { |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime _) {}, |
| showDayOfWeek: true, |
| ), |
| returnsNormally, |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime _) {}, |
| showDayOfWeek: true, |
| ), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('showDayOfWeek is only supported in date mode'), |
| ), |
| ), |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (DateTime _) {}, |
| showDayOfWeek: true, |
| ), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('showDayOfWeek is only supported in date mode'), |
| ), |
| ), |
| ); |
| |
| expect( |
| () => CupertinoDatePicker(onDateTimeChanged: (DateTime _) {}, showDayOfWeek: true), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('showDayOfWeek is only supported in date mode'), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('picker honors minuteInterval and secondInterval', (WidgetTester tester) async { |
| late Duration duration; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoTimerPicker( |
| minuteInterval: 10, |
| secondInterval: 12, |
| initialTimerDuration: const Duration(hours: 10, minutes: 40, seconds: 48), |
| onTimerDurationChanged: (Duration d) { |
| duration = d; |
| }, |
| ), |
| ), |
| ), |
| ); |
| await tester.drag(find.text('40'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.drag(find.text('48'), -_kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(duration, const Duration(hours: 10, minutes: 50, seconds: 36)); |
| }); |
| |
| group('Date picker', () { |
| testWidgets('initial date is set to default value', (WidgetTester tester) async { |
| final picker = CupertinoDatePicker(onDateTimeChanged: (_) {}); |
| expect(picker.initialDateTime, isNotNull); |
| }); |
| |
| testWidgets('background color default value', (WidgetTester tester) async { |
| await tester.pumpWidget(CupertinoApp(home: CupertinoDatePicker(onDateTimeChanged: (_) {}))); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect(pickers.any((CupertinoPicker picker) => picker.backgroundColor != null), false); |
| }); |
| |
| testWidgets('background color can be null', (WidgetTester tester) async { |
| await tester.pumpWidget(CupertinoApp(home: CupertinoDatePicker(onDateTimeChanged: (_) {}))); |
| |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('specified background color is applied', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoDatePicker( |
| onDateTimeChanged: (_) {}, |
| backgroundColor: CupertinoColors.black, |
| ), |
| ), |
| ); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect( |
| pickers.any((CupertinoPicker picker) => picker.backgroundColor != CupertinoColors.black), |
| false, |
| ); |
| }); |
| |
| testWidgets('specified item extent value is applied', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp(home: CupertinoDatePicker(itemExtent: 55, onDateTimeChanged: (_) {})), |
| ); |
| |
| final Iterable<CupertinoPicker> pickers = tester.allWidgets.whereType<CupertinoPicker>(); |
| expect(pickers.any((CupertinoPicker picker) => picker.itemExtent != 55), false); |
| }); |
| |
| testWidgets('initial date honors minuteInterval', (WidgetTester tester) async { |
| late DateTime newDateTime; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| width: 400, |
| height: 400, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime d) => newDateTime = d, |
| initialDateTime: DateTime(2018, 10, 10, 10, 3), |
| minuteInterval: 3, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Drag the minute picker to the next slot (03 -> 06). |
| // The `initialDateTime` and the `minuteInterval` values are specifically chosen |
| // so that `find.text` finds exactly one widget. |
| await tester.drag(find.text('03'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| |
| expect(newDateTime.minute, 6); |
| }); |
| |
| test('initial date honors minimumDate & maximumDate', () { |
| expect(() { |
| CupertinoDatePicker( |
| onDateTimeChanged: (DateTime d) {}, |
| initialDateTime: DateTime(2018, 10, 10), |
| minimumDate: DateTime(2018, 10, 11), |
| ); |
| }, throwsAssertionError); |
| |
| expect(() { |
| CupertinoDatePicker( |
| onDateTimeChanged: (DateTime d) {}, |
| initialDateTime: DateTime(2018, 10, 10), |
| maximumDate: DateTime(2018, 10, 9), |
| ); |
| }, throwsAssertionError); |
| }); |
| |
| testWidgets('changing initialDateTime after first build does not do anything', ( |
| WidgetTester tester, |
| ) async { |
| late DateTime selectedDateTime; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag( |
| find.text('10'), |
| const Offset(0.0, 32.0), |
| pointer: 1, |
| touchSlopY: 0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(selectedDateTime, DateTime(2018, 1, 1, 9, 30)); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime dateTime) => selectedDateTime = dateTime, |
| // Change the initial date, but it shouldn't affect the present state. |
| initialDateTime: DateTime(2016, 4, 5, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag( |
| find.text('9'), |
| const Offset(0.0, 32.0), |
| pointer: 1, |
| touchSlopY: 0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| // Moving up an hour is still based on the original initial date time. |
| expect(selectedDateTime, DateTime(2018, 1, 1, 8, 30)); |
| }); |
| |
| testWidgets('date picker has expected string', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('September'), findsOneWidget); |
| expect(find.text('9'), findsOneWidget); |
| expect(find.text('2018'), findsOneWidget); |
| }); |
| |
| testWidgets('datetime picker has expected string', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 9, 15, 3, 14), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Sat Sep 15'), findsOneWidget); |
| expect(find.text('3'), findsOneWidget); |
| expect(find.text('14'), findsOneWidget); |
| expect(find.text('AM'), findsOneWidget); |
| }); |
| |
| testWidgets('monthYear picker has expected string', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 9), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('September'), findsOneWidget); |
| expect(find.text('2018'), findsOneWidget); |
| }); |
| |
| testWidgets('width of picker in date and time mode is consistent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first column and the last column. |
| final double distance = |
| tester.getCenter(find.text('Mon Jan 1 ')).dx - tester.getCenter(find.text('AM')).dx; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 800.0, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first and the last column should be the same. |
| expect( |
| tester.getCenter(find.text('Mon Jan 1 ')).dx - tester.getCenter(find.text('AM')).dx, |
| distance, |
| ); |
| }); |
| |
| testWidgets('width of picker in date mode is consistent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first column and the last column. |
| final double distance = |
| tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 800.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first and the last column should be the same. |
| expect( |
| tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx, |
| distance, |
| ); |
| }); |
| |
| testWidgets('width of picker in time mode is consistent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first column and the last column. |
| final double distance = |
| tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 800.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first and the last column should be the same. |
| expect( |
| tester.getCenter(find.text('10')).dx - tester.getCenter(find.text('AM')).dx, |
| moreOrLessEquals(distance), |
| ); |
| }); |
| |
| testWidgets('width of picker in monthYear mode is consistent', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first column and the last column. |
| final double distance = |
| tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 800.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Distance between the first and the last column should be the same. |
| expect( |
| tester.getCenter(find.text('January')).dx - tester.getCenter(find.text('2018')).dx, |
| distance, |
| ); |
| }); |
| |
| testWidgets('wheel does not bend outwards', (WidgetTester tester) async { |
| final Widget dateWidget = CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 1, 1, 10, 30), |
| ); |
| |
| const centerMonth = 'January'; |
| const visibleMonthsExceptTheCenter = <String>[ |
| 'September', |
| 'October', |
| 'November', |
| 'December', |
| 'February', |
| 'March', |
| 'April', |
| 'May', |
| ]; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: Center(child: SizedBox(height: 200.0, width: 300.0, child: dateWidget)), |
| ), |
| ), |
| ); |
| |
| // The wheel does not bend outwards. |
| for (final month in visibleMonthsExceptTheCenter) { |
| expect( |
| tester.getBottomLeft(find.text(centerMonth)).dx, |
| lessThan(tester.getBottomLeft(find.text(month)).dx), |
| ); |
| } |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoPageScaffold( |
| child: Center(child: SizedBox(height: 200.0, width: 3000.0, child: dateWidget)), |
| ), |
| ), |
| ); |
| |
| // The wheel does not bend outwards at large widths. |
| for (final month in visibleMonthsExceptTheCenter) { |
| expect( |
| tester.getBottomLeft(find.text(centerMonth)).dx, |
| lessThan(tester.getBottomLeft(find.text(month)).dx), |
| ); |
| } |
| }); |
| |
| testWidgets('non-selectable dates are greyed out, ' |
| 'when minimum date is unconstrained', (WidgetTester tester) async { |
| final maximum = DateTime(2018, 6, 15); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| maximumDate: maximum, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 6, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // unconstrained bounds are not affected. |
| expect( |
| tester.widget<Text>(find.text('14')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| // the selected day is not affected. |
| expect( |
| tester.widget<Text>(find.text('15')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| // out of bounds and should be greyed out. |
| expect( |
| tester.widget<Text>(find.text('16')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| }); |
| |
| testWidgets('non-selectable dates are greyed out, ' |
| 'when maximum date is unconstrained', (WidgetTester tester) async { |
| final minimum = DateTime(2018, 6, 15); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| minimumDate: minimum, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 6, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // out of bounds and should be greyed out. |
| expect( |
| tester.widget<Text>(find.text('14')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| // the selected day is not affected. |
| expect( |
| tester.widget<Text>(find.text('15')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| // unconstrained bounds are not affected. |
| expect( |
| tester.widget<Text>(find.text('16')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| }); |
| |
| testWidgets('non-selectable dates are greyed out, ' |
| 'months should be taken into account when greying out days', (WidgetTester tester) async { |
| final minimum = DateTime(2018, 5, 15); |
| final maximum = DateTime(2018, 7, 15); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 6, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // days of a different min/max month are not affected. |
| expect( |
| tester.widget<Text>(find.text('14')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| expect( |
| tester.widget<Text>(find.text('16')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| }); |
| |
| testWidgets('non-selectable dates are greyed out, ' |
| 'years should be taken into account when greying out days', (WidgetTester tester) async { |
| final minimum = DateTime(2017, 6, 15); |
| final maximum = DateTime(2019, 6, 15); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 6, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // days of a different min/max year are not affected. |
| expect( |
| tester.widget<Text>(find.text('14')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| expect( |
| tester.widget<Text>(find.text('16')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| }); |
| |
| testWidgets('picker automatically scrolls away from invalid date on month change', ( |
| WidgetTester tester, |
| ) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2018, 3, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag( |
| find.text('March'), |
| const Offset(0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| |
| // Momentarily, the 2018 and the incorrect 30 of February is aligned. |
| expect(tester.getTopLeft(find.text('2018')).dy, tester.getTopLeft(find.text('30')).dy); |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pump(); // Once to start the DrivenScrollActivity. |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 2, 28)); |
| expect(tester.getTopLeft(find.text('2018')).dy, tester.getTopLeft(find.text('28')).dy); |
| }); |
| |
| testWidgets('date picker automatically scrolls away from invalid date, ' |
| "and onDateTimeChanged doesn't report these dates", (WidgetTester tester) async { |
| late DateTime date; |
| // 2016 is a leap year. |
| final minimum = DateTime(2016, 2, 29); |
| final maximum = DateTime(2018, 12, 31); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| // Callback doesn't transiently go into invalid dates. |
| expect(newDate.isAtSameMomentAs(minimum) || newDate.isAfter(minimum), isTrue); |
| expect(newDate.isAtSameMomentAs(maximum) || newDate.isBefore(maximum), isTrue); |
| }, |
| initialDateTime: DateTime(2017, 2, 28), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 2017 has 28 days in Feb so 29 is greyed out. |
| expect( |
| tester.widget<Text>(find.text('29')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| await tester.drag( |
| find.text('2017'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pumpAndSettle(); // Now the autoscrolling should happen. |
| |
| expect(date, DateTime(2016, 2, 29)); |
| |
| // 2016 has 29 days in Feb so 29 is not greyed out. |
| expect( |
| tester.widget<Text>(find.text('29')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| await tester.drag( |
| find.text('2016'), |
| const Offset(0.0, -32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pumpAndSettle(); |
| |
| expect(date, DateTime(2017, 2, 28)); |
| |
| expect( |
| tester.widget<Text>(find.text('29')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| }); |
| |
| testWidgets('dateTime picker automatically scrolls away from invalid date, ' |
| "and onDateTimeChanged doesn't report these dates", (WidgetTester tester) async { |
| late DateTime date; |
| final minimum = DateTime(2019, 11, 11, 3, 30); |
| final maximum = DateTime(2019, 11, 11, 14, 59, 59); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| // Callback doesn't transiently go into invalid dates. |
| expect(minimum.isAfter(newDate), isFalse); |
| expect(maximum.isBefore(newDate), isFalse); |
| }, |
| initialDateTime: DateTime(2019, 11, 11, 4), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 3:00 is valid but 2:00 should be invalid. |
| expect( |
| tester.widget<Text>(find.text('3')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| expect( |
| tester.widget<Text>(find.text('2')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| // 'PM' is greyed out. |
| expect( |
| tester.widget<Text>(find.text('PM')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| await tester.drag( |
| find.text('AM'), |
| const Offset(0.0, -32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pumpAndSettle(); // Now the autoscrolling should happen. |
| |
| expect(date, DateTime(2019, 11, 11, 14, 59)); |
| |
| // 3'o clock and 'AM' are now greyed out. |
| expect( |
| tester.widget<Text>(find.text('AM')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| expect( |
| tester.widget<Text>(find.text('3')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| await tester.drag( |
| find.text('PM'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pumpAndSettle(); |
| |
| // Returns to min date. |
| expect(date, DateTime(2019, 11, 11, 3, 30)); |
| }); |
| |
| testWidgets('time picker automatically scrolls away from invalid date, ' |
| "and onDateTimeChanged doesn't report these dates", (WidgetTester tester) async { |
| late DateTime date; |
| final minimum = DateTime(2019, 11, 11, 3, 30); |
| final maximum = DateTime(2019, 11, 11, 14, 59, 59); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| // Callback doesn't transiently go into invalid dates. |
| expect(minimum.isAfter(newDate), isFalse); |
| expect(maximum.isBefore(newDate), isFalse); |
| }, |
| initialDateTime: DateTime(2019, 11, 11, 4), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 3:00 is valid but 2:00 should be invalid. |
| expect( |
| tester.widget<Text>(find.text('3')).style!.color, |
| isNot(isSameColorAs(CupertinoColors.inactiveGray.color)), |
| ); |
| |
| expect( |
| tester.widget<Text>(find.text('2')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| // 'PM' is greyed out. |
| expect( |
| tester.widget<Text>(find.text('PM')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| await tester.drag( |
| find.text('AM'), |
| const Offset(0.0, -32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pumpAndSettle(); // Now the autoscrolling should happen. |
| |
| expect(date, DateTime(2019, 11, 11, 14, 59)); |
| |
| // 3'o clock and 'AM' are now greyed out. |
| expect( |
| tester.widget<Text>(find.text('AM')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| expect( |
| tester.widget<Text>(find.text('3')).style!.color, |
| isSameColorAs(CupertinoColors.inactiveGray.color), |
| ); |
| |
| await tester.drag( |
| find.text('PM'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pumpAndSettle(); |
| |
| // Returns to min date. |
| expect(date, DateTime(2019, 11, 11, 3, 30)); |
| }); |
| |
| testWidgets('monthYear picker automatically scrolls away from invalid date, ' |
| "and onDateTimeChanged doesn't report these dates", (WidgetTester tester) async { |
| late DateTime date; |
| final minimum = DateTime(2016, 2); |
| final maximum = DateTime(2018, 12); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| // Callback doesn't transiently go into invalid dates. |
| expect(newDate.isAtSameMomentAs(minimum) || newDate.isAfter(minimum), isTrue); |
| expect(newDate.isAtSameMomentAs(maximum) || newDate.isBefore(maximum), isTrue); |
| }, |
| initialDateTime: DateTime(2017, 2), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag( |
| find.text('2017'), |
| const Offset(0.0, 100.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pumpAndSettle(); // Now the autoscrolling should happen. |
| |
| expect(date, DateTime(2016, 2)); |
| |
| await tester.drag( |
| find.text('2016'), |
| const Offset(0.0, -100.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pumpAndSettle(); |
| |
| expect(date, DateTime(2018, 12)); |
| |
| await tester.drag( |
| find.text('2016'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| await tester.pumpAndSettle(); |
| |
| expect(date, DateTime(2017, 12)); |
| }); |
| |
| testWidgets('picker automatically scrolls away from invalid date on day change', ( |
| WidgetTester tester, |
| ) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2018, 2, 27), // 2018 has 28 days in Feb. |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag( |
| find.text('27'), |
| const Offset(0.0, -32.0), |
| pointer: 1, |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| expect(date, DateTime(2018, 2, 28)); |
| |
| await tester.drag( |
| find.text('28'), |
| const Offset(0.0, -32.0), |
| pointer: 1, |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); // Once to trigger the post frame animate call. |
| |
| // Callback doesn't transiently go into invalid dates. |
| expect(date, DateTime(2018, 2, 28)); |
| // Momentarily, the invalid 29th of Feb is dragged into the middle. |
| expect(tester.getTopLeft(find.text('2018')).dy, tester.getTopLeft(find.text('29')).dy); |
| |
| await tester.pump(); // Once to start the DrivenScrollActivity. |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 2, 28)); |
| expect(tester.getTopLeft(find.text('2018')).dy, tester.getTopLeft(find.text('28')).dy); |
| }); |
| |
| testWidgets( |
| 'date picker should only take into account the date part of minimumDate and maximumDate', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/49606. |
| late DateTime date; |
| final minDate = DateTime(2020, 1, 1, 12); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| minimumDate: minDate, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2020, 1, 12), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Scroll to 2019. |
| await tester.drag( |
| find.text('2020'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pumpAndSettle(); |
| expect(date.year, minDate.year); |
| expect(date.month, minDate.month); |
| expect(date.day, minDate.day); |
| }, |
| ); |
| |
| testWidgets( |
| 'date picker does not display previous day of minimumDate if it is set at midnight', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/72932 |
| final minDate = DateTime(2019, 12, 31); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| minimumDate: minDate, |
| onDateTimeChanged: (DateTime newDate) {}, |
| initialDateTime: minDate.add(const Duration(days: 1)), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('Mon Dec 30'), findsNothing); |
| }, |
| ); |
| |
| group('Picker handles initial noon/midnight times', () { |
| testWidgets('midnight', (WidgetTester tester) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2019, 1, 1, 0, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 0:15 -> 0:16 |
| await tester.drag(find.text('15'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 0, 16)); |
| }); |
| |
| testWidgets('noon', (WidgetTester tester) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2019, 1, 1, 12, 15), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 12:15 -> 12:16 |
| await tester.drag(find.text('15'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 12, 16)); |
| }); |
| |
| testWidgets('noon in 24 hour time', (WidgetTester tester) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| use24hFormat: true, |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2019, 1, 1, 12, 25), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 12:25 -> 12:26 |
| await tester.drag(find.text('25'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 12, 26)); |
| }); |
| }); |
| |
| testWidgets('picker persists am/pm value when scrolling hours', (WidgetTester tester) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2019, 1, 1, 3), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // 3:00 -> 15:00 |
| await tester.drag(find.text('AM'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 15)); |
| |
| // 15:00 -> 16:00 |
| await tester.drag(find.text('3'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 16)); |
| |
| // 16:00 -> 4:00 |
| await tester.drag(find.text('PM'), -_kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 4)); |
| |
| // 4:00 -> 3:00 |
| await tester.drag(find.text('4'), -_kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2019, 1, 1, 3)); |
| }); |
| |
| testWidgets( |
| 'picker automatically scrolls the am/pm column when the hour column changes enough', |
| (WidgetTester tester) async { |
| late DateTime date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| initialDateTime: DateTime(2018, 1, 1, 11, 59), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| const deltaOffset = Offset(0.0, -18.0); |
| |
| // 11:59 -> 12:59 |
| await tester.drag(find.text('11'), _kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 1, 1, 12, 59)); |
| |
| // 12:59 -> 11:59 |
| await tester.drag(find.text('12'), -_kRowOffset, warnIfMissed: false); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 1, 1, 11, 59)); |
| |
| // 11:59 -> 9:59 |
| await tester.drag( |
| find.text('11'), |
| -((_kRowOffset - deltaOffset) * 2 + deltaOffset), |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 1, 1, 9, 59)); |
| |
| // 9:59 -> 15:59 |
| await tester.drag( |
| find.text('9'), |
| (_kRowOffset - deltaOffset) * 6 + deltaOffset, |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 500)); |
| |
| expect(date, DateTime(2018, 1, 1, 15, 59)); |
| }, |
| ); |
| |
| testWidgets('date picker given too narrow space horizontally shows message', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| // This is too small to draw the picker out fully. |
| width: 100, |
| child: CupertinoDatePicker( |
| initialDateTime: DateTime(2019, 1, 1, 4), |
| onDateTimeChanged: (_) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| expect( |
| exception.toString(), |
| contains('Insufficient horizontal space to render the CupertinoDatePicker'), |
| ); |
| }); |
| |
| testWidgets('DatePicker golden tests', (WidgetTester tester) async { |
| Widget buildApp(CupertinoDatePickerMode mode) { |
| return CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| width: 500, |
| height: 400, |
| child: RepaintBoundary( |
| child: CupertinoDatePicker( |
| key: ValueKey<CupertinoDatePickerMode>(mode), |
| mode: mode, |
| initialDateTime: DateTime(2019, 1, 1, 4, 12, 30), |
| onDateTimeChanged: (_) {}, |
| ), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildApp(CupertinoDatePickerMode.time)); |
| await expectLater( |
| find.byType(CupertinoDatePicker), |
| matchesGoldenFile('date_picker_test.time.initial.png'), |
| ); |
| |
| await tester.pumpWidget(buildApp(CupertinoDatePickerMode.date)); |
| await expectLater( |
| find.byType(CupertinoDatePicker), |
| matchesGoldenFile('date_picker_test.date.initial.png'), |
| ); |
| |
| await tester.pumpWidget(buildApp(CupertinoDatePickerMode.monthYear)); |
| await expectLater( |
| find.byType(CupertinoDatePicker), |
| matchesGoldenFile('date_picker_test.monthyear.initial.png'), |
| ); |
| |
| await tester.pumpWidget(buildApp(CupertinoDatePickerMode.dateAndTime)); |
| await expectLater( |
| find.byType(CupertinoDatePicker), |
| matchesGoldenFile('date_picker_test.datetime.initial.png'), |
| ); |
| |
| // Slightly drag the hour component to make the current hour off-center. |
| await tester.drag( |
| find.text('4'), |
| Offset(0, _kRowOffset.dy / 2), |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| |
| await expectLater( |
| find.byType(CupertinoDatePicker), |
| matchesGoldenFile('date_picker_test.datetime.drag.png'), |
| ); |
| }); |
| |
| testWidgets('DatePicker displays the date in correct order', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| dateOrder: DatePickerDateOrder.ydm, |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime newDate) {}, |
| initialDateTime: DateTime(2018, 1, 14, 10, 30), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getTopLeft(find.text('2018')).dx, |
| lessThan(tester.getTopLeft(find.text('14')).dx), |
| ); |
| |
| expect( |
| tester.getTopLeft(find.text('14')).dx, |
| lessThan(tester.getTopLeft(find.text('January')).dx), |
| ); |
| }); |
| |
| testWidgets('monthYear DatePicker displays the date in correct order', ( |
| WidgetTester tester, |
| ) async { |
| Widget buildApp(DatePickerDateOrder order) { |
| return CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| key: ValueKey<DatePickerDateOrder>(order), |
| dateOrder: order, |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (DateTime newDate) {}, |
| initialDateTime: DateTime(2018, 1, 14, 10, 30), |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildApp(DatePickerDateOrder.dmy)); |
| expect( |
| tester.getTopLeft(find.text('January')).dx, |
| lessThan(tester.getTopLeft(find.text('2018')).dx), |
| ); |
| |
| await tester.pumpWidget(buildApp(DatePickerDateOrder.mdy)); |
| expect( |
| tester.getTopLeft(find.text('January')).dx, |
| lessThan(tester.getTopLeft(find.text('2018')).dx), |
| ); |
| |
| await tester.pumpWidget(buildApp(DatePickerDateOrder.ydm)); |
| expect( |
| tester.getTopLeft(find.text('2018')).dx, |
| lessThan(tester.getTopLeft(find.text('January')).dx), |
| ); |
| |
| await tester.pumpWidget(buildApp(DatePickerDateOrder.ymd)); |
| expect( |
| tester.getTopLeft(find.text('2018')).dx, |
| lessThan(tester.getTopLeft(find.text('January')).dx), |
| ); |
| }); |
| |
| testWidgets('DatePicker displays hours and minutes correctly in RTL', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Directionality( |
| textDirection: TextDirection.rtl, |
| child: Center( |
| child: SizedBox( |
| width: 500, |
| height: 400, |
| child: CupertinoDatePicker( |
| initialDateTime: DateTime(2019, 1, 1, 4), |
| onDateTimeChanged: (_) {}, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final double hourLeft = tester.getTopLeft(find.text('4')).dx; |
| final double minuteLeft = tester.getTopLeft(find.text('00')).dx; |
| expect(hourLeft, lessThan(minuteLeft)); |
| }); |
| |
| testWidgets('onScrollEnd behavior reports changes correctly', (WidgetTester tester) async { |
| final selectedDateTime = <DateTime>[]; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| changeReportingBehavior: ChangeReportingBehavior.onScrollEnd, |
| onDateTimeChanged: (DateTime dateTime) => selectedDateTime.add(dateTime), |
| initialDateTime: DateTime(2025), |
| ), |
| ), |
| ), |
| ), |
| ); |
| final Offset initialOffset = tester.getTopLeft(find.text('2025')); |
| |
| final TestGesture scrollGesture = await tester.startGesture(initialOffset); |
| // Should not report changes until the gesture ends. |
| await scrollGesture.moveBy(const Offset(0.0, -32.0)); |
| expect(selectedDateTime, isEmpty); |
| |
| await scrollGesture.moveBy(const Offset(0.0, -32.0)); |
| expect(selectedDateTime, isEmpty); |
| |
| await scrollGesture.up(); |
| await tester.pumpAndSettle(); |
| |
| // Only reports the last change. |
| expect(selectedDateTime, hasLength(1)); |
| expect(selectedDateTime.first, DateTime(2027)); |
| }); |
| }); |
| |
| testWidgets('TimerPicker golden tests', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| // Also check if the picker respects the theme. |
| theme: const CupertinoThemeData( |
| textTheme: CupertinoTextThemeData( |
| pickerTextStyle: TextStyle(color: Color(0xFF663311), fontSize: 21), |
| ), |
| ), |
| home: Center( |
| child: SizedBox( |
| width: 320, |
| height: 216, |
| child: RepaintBoundary( |
| child: CupertinoTimerPicker( |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 23, minutes: 59), |
| onTimerDurationChanged: (_) {}, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| await expectLater( |
| find.byType(CupertinoTimerPicker), |
| matchesGoldenFile('timer_picker_test.datetime.initial.png'), |
| ); |
| |
| // Slightly drag the minute component to make the current minute off-center. |
| await tester.drag( |
| find.text('59'), |
| Offset(0, _kRowOffset.dy / 2), |
| warnIfMissed: false, |
| ); // see top of file |
| await tester.pump(); |
| |
| await expectLater( |
| find.byType(CupertinoTimerPicker), |
| matchesGoldenFile('timer_picker_test.datetime.drag.png'), |
| ); |
| }); |
| |
| testWidgets('TimerPicker only changes hour label after scrolling stops', ( |
| WidgetTester tester, |
| ) async { |
| Duration? duration; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| width: 320, |
| height: 216, |
| child: CupertinoTimerPicker( |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 2, minutes: 30), |
| onTimerDurationChanged: (Duration d) { |
| duration = d; |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(duration, isNull); |
| expect(find.text('hour'), findsNothing); |
| expect(find.text('hours'), findsOneWidget); |
| |
| await tester.drag( |
| find.text('2'), |
| Offset(0, -_kRowOffset.dy), |
| warnIfMissed: false, |
| ); // see top of file |
| // Duration should change but not the label. |
| expect(duration!.inHours, 1); |
| expect(find.text('hour'), findsNothing); |
| expect(find.text('hours'), findsOneWidget); |
| await tester.pumpAndSettle(); |
| |
| // Now the label should change. |
| expect(duration!.inHours, 1); |
| expect(find.text('hours'), findsNothing); |
| expect(find.text('hour'), findsOneWidget); |
| }); |
| |
| testWidgets('TimerPicker has intrinsic width and height', (WidgetTester tester) async { |
| const key = Key('key'); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoTimerPicker( |
| key: key, |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 2, minutes: 30), |
| onTimerDurationChanged: (Duration d) {}, |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), |
| const Size(320, 216), |
| ); |
| |
| // Different modes shouldn't share state. |
| await tester.pumpWidget(const Placeholder()); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoTimerPicker( |
| key: key, |
| mode: CupertinoTimerPickerMode.ms, |
| initialTimerDuration: const Duration(minutes: 30, seconds: 3), |
| onTimerDurationChanged: (Duration d) {}, |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), |
| const Size(320, 216), |
| ); |
| |
| // Different modes shouldn't share state. |
| await tester.pumpWidget(const Placeholder()); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: CupertinoTimerPicker( |
| key: key, |
| initialTimerDuration: const Duration(hours: 5, minutes: 17, seconds: 19), |
| onTimerDurationChanged: (Duration d) {}, |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.getSize(find.descendant(of: find.byKey(key), matching: find.byType(Row))), |
| const Size(342, 216), |
| ); |
| }); |
| |
| testWidgets('scrollController can be removed or added', (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| late int lastSelectedItem; |
| void onSelectedItemChanged(int index) { |
| lastSelectedItem = index; |
| } |
| |
| final scrollController1 = FixedExtentScrollController(); |
| addTearDown(scrollController1.dispose); |
| await tester.pumpWidget( |
| _buildPicker(controller: scrollController1, onSelectedItemChanged: onSelectedItemChanged), |
| ); |
| |
| tester.binding.pipelineOwner.semanticsOwner!.performAction(1, SemanticsAction.increase); |
| await tester.pumpAndSettle(); |
| expect(lastSelectedItem, 1); |
| |
| await tester.pumpWidget(_buildPicker(onSelectedItemChanged: onSelectedItemChanged)); |
| |
| tester.binding.pipelineOwner.semanticsOwner!.performAction(1, SemanticsAction.increase); |
| await tester.pumpAndSettle(); |
| expect(lastSelectedItem, 2); |
| |
| final scrollController2 = FixedExtentScrollController(); |
| addTearDown(scrollController2.dispose); |
| await tester.pumpWidget( |
| _buildPicker(controller: scrollController2, onSelectedItemChanged: onSelectedItemChanged), |
| ); |
| |
| tester.binding.pipelineOwner.semanticsOwner!.performAction(1, SemanticsAction.increase); |
| await tester.pumpAndSettle(); |
| expect(lastSelectedItem, 3); |
| |
| handle.dispose(); |
| }); |
| |
| testWidgets('CupertinoDataPicker does not provide invalid MediaQuery', ( |
| WidgetTester tester, |
| ) async { |
| // Regression test for https://github.com/flutter/flutter/issues/47989. |
| Brightness brightness = Brightness.light; |
| late StateSetter setState; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| theme: const CupertinoThemeData( |
| textTheme: CupertinoTextThemeData( |
| dateTimePickerTextStyle: TextStyle( |
| color: CupertinoDynamicColor.withBrightness( |
| color: Color(0xFFFFFFFF), |
| darkColor: Color(0xFF000000), |
| ), |
| ), |
| ), |
| ), |
| home: StatefulBuilder( |
| builder: (BuildContext context, StateSetter stateSetter) { |
| setState = stateSetter; |
| return MediaQuery( |
| data: MediaQuery.of(context).copyWith(platformBrightness: brightness), |
| child: CupertinoDatePicker( |
| initialDateTime: DateTime(2019), |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime date) {}, |
| ), |
| ); |
| }, |
| ), |
| ), |
| ); |
| |
| expect( |
| tester.widget<Text>(find.text('2019')).style!.color, |
| isSameColorAs(const Color(0xFFFFFFFF)), |
| ); |
| |
| setState(() { |
| brightness = Brightness.dark; |
| }); |
| await tester.pump(); |
| |
| expect( |
| tester.widget<Text>(find.text('2019')).style!.color, |
| isSameColorAs(const Color(0xFF000000)), |
| ); |
| }); |
| |
| testWidgets('picker exports semantics', (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| debugResetSemanticsIdCounter(); |
| int? lastSelectedItem; |
| await tester.pumpWidget( |
| _buildPicker( |
| onSelectedItemChanged: (int index) { |
| lastSelectedItem = index; |
| }, |
| ), |
| ); |
| |
| expect( |
| tester.getSemantics(find.byType(CupertinoPicker)), |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| hasIncreaseAction: true, |
| increasedValue: '1', |
| value: '0', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ); |
| |
| tester.binding.pipelineOwner.semanticsOwner!.performAction(1, SemanticsAction.increase); |
| await tester.pumpAndSettle(); |
| |
| expect( |
| tester.getSemantics(find.byType(CupertinoPicker)), |
| matchesSemantics( |
| children: <Matcher>[ |
| matchesSemantics( |
| hasIncreaseAction: true, |
| hasDecreaseAction: true, |
| increasedValue: '2', |
| decreasedValue: '0', |
| value: '1', |
| textDirection: TextDirection.ltr, |
| ), |
| ], |
| ), |
| ); |
| expect(lastSelectedItem, 1); |
| handle.dispose(); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/98567 |
| testWidgets('picker semantics action test', (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| debugResetSemanticsIdCounter(); |
| final initialDate = DateTime(2018, 6, 8); |
| late DateTime? date; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime newDate) => date = newDate, |
| initialDateTime: initialDate, |
| maximumDate: initialDate.add(const Duration(days: 2)), |
| minimumDate: initialDate.subtract(const Duration(days: 2)), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| tester.binding.pipelineOwner.semanticsOwner!.performAction(4, SemanticsAction.decrease); |
| await tester.pumpAndSettle(); |
| |
| expect(date, DateTime(2018, 6, 7)); |
| |
| handle.dispose(); |
| }); |
| |
| testWidgets('CupertinoDatePicker semantics excludes disabled dates', (WidgetTester tester) async { |
| final SemanticsHandle handle = tester.ensureSemantics(); |
| debugResetSemanticsIdCounter(); |
| final minimumDate = DateTime(2018, 6, 10); |
| final maximumDate = DateTime(2018, 6, 20); |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| minimumDate: minimumDate, |
| maximumDate: maximumDate, |
| initialDateTime: minimumDate, // Start at minimum date |
| onDateTimeChanged: (DateTime newDateTime) {}, |
| mode: CupertinoDatePickerMode.date, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the day picker column semantics node |
| // The day picker should have increase action (to go to day 11) but NO decrease action |
| // (because day 9 is disabled and wrapped with ExcludeSemantics) |
| final SemanticsNode rootNode = tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!; |
| |
| // Find semantics node with value '10' (the current day) |
| SemanticsNode? findNodeWithValue(SemanticsNode node, String value) { |
| if (node.value == value) { |
| return node; |
| } |
| SemanticsNode? result; |
| node.visitChildren((SemanticsNode child) { |
| result ??= findNodeWithValue(child, value); |
| return result == null; |
| }); |
| return result; |
| } |
| |
| final SemanticsNode? dayPickerNode = findNodeWithValue(rootNode, '10'); |
| expect(dayPickerNode, isNotNull, reason: 'Should find day picker at day 10'); |
| |
| // At the minimum date (day 10), the day picker should NOT have a decrease action |
| // because day 9 is disabled (wrapped with ExcludeSemantics) |
| final SemanticsData data = dayPickerNode!.getSemanticsData(); |
| expect( |
| data.hasAction(SemanticsAction.decrease), |
| isFalse, |
| reason: 'Day picker at minimum date should not have decrease action (day 9 is disabled)', |
| ); |
| expect( |
| data.hasAction(SemanticsAction.increase), |
| isTrue, |
| reason: 'Day picker at minimum date should have increase action (day 11 is valid)', |
| ); |
| |
| handle.dispose(); |
| }); |
| |
| // TODO(justinmc): Don't test Material interactions in Cupertino tests. |
| // https://github.com/flutter/flutter/issues/177028 |
| testWidgets('DatePicker adapts to MaterialApp dark mode', (WidgetTester tester) async { |
| Widget buildDatePicker(Brightness brightness) { |
| return MaterialApp( |
| theme: ThemeData(brightness: brightness), |
| home: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime neData) {}, |
| initialDateTime: DateTime(2018, 10, 10), |
| ), |
| ); |
| } |
| |
| // CupertinoDatePicker with light theme. |
| await tester.pumpWidget(buildDatePicker(Brightness.light)); |
| RenderParagraph paragraph = tester.renderObject(find.text('October').first); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| |
| // CupertinoDatePicker with dark theme. |
| await tester.pumpWidget(buildDatePicker(Brightness.dark)); |
| paragraph = tester.renderObject(find.text('October').first); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| }); |
| |
| // TODO(justinmc): Don't test Material interactions in Cupertino tests. |
| // https://github.com/flutter/flutter/issues/177028 |
| testWidgets('TimerPicker adapts to MaterialApp dark mode', (WidgetTester tester) async { |
| Widget buildTimerPicker(Brightness brightness) { |
| return MaterialApp( |
| theme: ThemeData(brightness: brightness), |
| home: CupertinoTimerPicker( |
| mode: CupertinoTimerPickerMode.hm, |
| onTimerDurationChanged: (Duration newDuration) {}, |
| initialTimerDuration: const Duration(hours: 12, minutes: 30, seconds: 59), |
| ), |
| ); |
| } |
| |
| // CupertinoTimerPicker with light theme. |
| await tester.pumpWidget(buildTimerPicker(Brightness.light)); |
| RenderParagraph paragraph = tester.renderObject(find.text('hours')); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| |
| // CupertinoTimerPicker with light theme. |
| await tester.pumpWidget(buildTimerPicker(Brightness.dark)); |
| paragraph = tester.renderObject(find.text('hours')); |
| expect(paragraph.text.style!.color, CupertinoColors.label); |
| // Text style should not return unresolved color. |
| expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse); |
| }); |
| |
| testWidgets('TimerPicker minDate - maxDate with minuteInterval', (WidgetTester tester) async { |
| late DateTime date; |
| final minimum = DateTime(2022, 6, 14, 3, 31); |
| final initial = DateTime(2022, 6, 14, 3, 40); |
| final maximum = DateTime(2022, 6, 14, 3, 49); |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| initialDateTime: initial, |
| minimumDate: minimum, |
| maximumDate: maximum, |
| minuteInterval: 5, |
| use24hFormat: true, |
| onDateTimeChanged: (DateTime newDate) { |
| date = newDate; |
| }, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| // Drag picker minutes to min date |
| await tester.drag( |
| find.text('40'), |
| const Offset(0.0, 32.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Returns to min date. |
| expect(date, DateTime(2022, 6, 14, 3, 35)); |
| |
| // Drag picker minutes to max date |
| await tester.drag( |
| find.text('50'), |
| const Offset(0.0, -64.0), |
| touchSlopY: 0.0, |
| warnIfMissed: false, |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Returns to max date. |
| expect(date, DateTime(2022, 6, 14, 3, 45)); |
| }); |
| |
| testWidgets('date picker has expected day of week', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| height: 400.0, |
| width: 400.0, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| showDayOfWeek: true, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text('September'), findsOneWidget); |
| expect(find.textContaining('Sat').last, findsOneWidget); |
| expect(find.textContaining('15').last, findsOneWidget); |
| expect(find.text('2018'), findsOneWidget); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder with monthYear mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoDatePickerMode.monthYear |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(2)); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder with date mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoDatePickerMode.date |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(3)); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder with time mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoDatePickerMode.time |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(3)); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder with dateAndTime mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoDatePickerMode.dateAndTime |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(4)); |
| }); |
| |
| testWidgets('CupertinoTimerPicker selectionOverlayBuilder with hms mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoTimerPickerMode.hms |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (Duration duration) {}, |
| initialTimerDuration: const Duration(hours: 1, minutes: 1, seconds: 1), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(3)); |
| }); |
| |
| testWidgets('CupertinoTimerPicker selectionOverlayBuilder with ms mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoTimerPickerMode.ms |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (Duration duration) {}, |
| mode: CupertinoTimerPickerMode.ms, |
| initialTimerDuration: const Duration(hours: 1, minutes: 1, seconds: 1), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(2)); |
| }); |
| |
| testWidgets('CupertinoTimerPicker selectionOverlayBuilder with hm mode', ( |
| WidgetTester tester, |
| ) async { |
| final Widget selectionOverlay = Container(color: const Color(0x12345678)); |
| |
| // For mode = CupertinoTimerPickerMode.hm |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (Duration duration) {}, |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 1, minutes: 1, seconds: 1), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return selectionOverlay; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Find the selection overlay. |
| expect(find.byWidget(selectionOverlay), findsExactly(2)); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder returns null', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return null; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), isNot(paints..rrect())); |
| }); |
| |
| testWidgets('CupertinoTimerPicker selectionOverlayBuilder returns null', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (Duration duration) {}, |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 1, minutes: 1, seconds: 1), |
| selectionOverlayBuilder: |
| (BuildContext context, {required int selectedIndex, required int columnCount}) { |
| return null; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPicker), isNot(paints..rrect())); |
| }); |
| |
| testWidgets('CupertinoTimerPicker selectionOverlayBuilder is explicitly passed null', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoTimerPicker( |
| onTimerDurationChanged: (Duration duration) {}, |
| mode: CupertinoTimerPickerMode.hm, |
| initialTimerDuration: const Duration(hours: 1, minutes: 1, seconds: 1), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPickerDefaultSelectionOverlay), findsExactly(2)); |
| }); |
| |
| testWidgets('CupertinoDatePicker selectionOverlayBuilder is explicitly passed null', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.byType(CupertinoPickerDefaultSelectionOverlay), findsExactly(4)); |
| }); |
| |
| testWidgets('CupertinoDatePicker accommodates widest text using table codepoints', ( |
| WidgetTester tester, |
| ) async { |
| // |---------| |
| // | 0x2002 | // EN SPACE - 1/2 Advance |
| // | 0x2005 | // FOUR-PER-EM SPACE - 1/4 Advance |
| // |---------| |
| final testWords = <String>[ |
| '\u2002' * 10, // Output: 10 * 1/2 = 5 |
| '\u2005' * 20, // Output: 20 * 1/4 = 5 |
| ]; |
| |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| onDateTimeChanged: (DateTime date) {}, |
| initialDateTime: DateTime(2018, 9, 15), |
| ), |
| ), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.byType(CupertinoDatePicker)); |
| |
| const textStyle = TextStyle( |
| fontSize: 21, |
| letterSpacing: 0.4, |
| fontWeight: FontWeight.normal, |
| color: CupertinoColors.label, |
| ); |
| |
| final List<double> widths = testWords |
| .map((String word) => getColumnWidth(word, textStyle, context)) |
| .toList(); |
| |
| final double largestWidth = widths.reduce(math.max); |
| |
| final double testWidth = CupertinoDatePicker.getColumnWidth( |
| texts: testWords, |
| context: context, |
| textStyle: textStyle, |
| ); |
| |
| expect(testWidth, equals(largestWidth)); |
| expect(widths.indexOf(largestWidth), equals(1)); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/39998 |
| |
| test('showTimeSeparator is only supported in time or dateAndTime mode', () async { |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime _) {}, |
| showTimeSeparator: true, |
| ), |
| returnsNormally, |
| ); |
| |
| expect( |
| () => CupertinoDatePicker(onDateTimeChanged: (DateTime _) {}, showTimeSeparator: true), |
| returnsNormally, |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (DateTime _) {}, |
| showTimeSeparator: true, |
| ), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('showTimeSeparator is only supported in time or dateAndTime modes'), |
| ), |
| ), |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.monthYear, |
| onDateTimeChanged: (DateTime _) {}, |
| showTimeSeparator: true, |
| ), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('showTimeSeparator is only supported in time or dateAndTime modes'), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('Time separator widget should be rendered when flag is set to true', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime dateTime) {}, |
| showTimeSeparator: true, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text(':'), findsOneWidget); |
| }); |
| |
| testWidgets('Time separator widget should not be rendered when flag is set to false', ( |
| WidgetTester tester, |
| ) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.time, |
| onDateTimeChanged: (DateTime _) {}, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(find.text(':'), findsNothing); |
| }); |
| |
| test('CupertinoDatePicker selectableDayPredicate parameter validation', () async { |
| expect(() => CupertinoDatePicker(onDateTimeChanged: (DateTime _) {}), returnsNormally); |
| |
| expect( |
| () => CupertinoDatePicker( |
| initialDateTime: DateTime(2025), |
| onDateTimeChanged: (DateTime _) {}, |
| selectableDayPredicate: (DateTime date) { |
| return date.year == 2025; |
| }, |
| ), |
| returnsNormally, |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| onDateTimeChanged: (DateTime _) {}, |
| selectableDayPredicate: (DateTime date) { |
| return date.year == 2025; |
| }, |
| ), |
| returnsNormally, |
| ); |
| |
| expect( |
| () => CupertinoDatePicker( |
| initialDateTime: DateTime(2025, 7, 4), |
| onDateTimeChanged: (DateTime _) {}, |
| selectableDayPredicate: (DateTime date) { |
| return date.month == 6; |
| }, |
| ), |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError e) => e.message ?? 'Unknown error', |
| 'message', |
| contains('must satisfy provided selectableDayPredicate.'), |
| ), |
| ), |
| ); |
| }); |
| |
| testWidgets('DatePicker with workdays predicate test case', (WidgetTester tester) async { |
| // Set initial date time to a work day. |
| final initialDateTime = DateTime(2025, 6, 13); |
| var selectedDate = initialDateTime; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| initialDateTime: initialDateTime, |
| selectableDayPredicate: (DateTime date) { |
| return date.weekday >= DateTime.monday && date.weekday <= DateTime.friday; |
| }, |
| onDateTimeChanged: (DateTime dateTime) { |
| selectedDate = dateTime; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Scrolling to Saturday should trigger automatic scroll to the next workday (Monday). |
| await tester.drag(find.text('Sat Jun 14'), const Offset(0.0, -100.0)); |
| expect(selectedDate, DateTime(2025, 6, 16)); |
| }); |
| |
| testWidgets('DatePicker with weekend predicate test case', (WidgetTester tester) async { |
| // Set initial date time to a weekend day. |
| final initialDateTime = DateTime(2025, 6, 14); |
| var selectedDate = initialDateTime; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| initialDateTime: initialDateTime, |
| selectableDayPredicate: (DateTime date) { |
| return date.weekday == DateTime.saturday || date.weekday == DateTime.sunday; |
| }, |
| onDateTimeChanged: (DateTime dateTime) { |
| selectedDate = dateTime; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| // Pressing on the friday day item should trigger automatic scroll back to |
| // saturday. |
| await tester.press(find.text('Fri Jun 13')); |
| await tester.pump(); |
| |
| expect(selectedDate, DateTime(2025, 6, 14)); |
| }); |
| |
| testWidgets('DatePicker with custom predicate test case', (WidgetTester tester) async { |
| // Set initial date time to a work day. |
| final initialDateTime = DateTime(2025, 6, 16); |
| var selectedDate = initialDateTime; |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: CupertinoDatePicker( |
| initialDateTime: initialDateTime, |
| selectableDayPredicate: (DateTime date) { |
| return date.day >= 16; |
| }, |
| onDateTimeChanged: (DateTime dateTime) { |
| selectedDate = dateTime; |
| }, |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.drag(find.text('Sun Jun 15'), const Offset(0.0, 64.0)); |
| await tester.pump(); |
| |
| expect(selectedDate, initialDateTime); |
| }); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/161773 |
| testWidgets('CupertinoDatePicker date value baseline alignment', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox( |
| width: 400, |
| height: 400, |
| child: CupertinoDatePicker( |
| mode: CupertinoDatePickerMode.date, |
| onDateTimeChanged: (_) {}, |
| initialDateTime: DateTime(2025, 2, 14), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| Offset lastOffset = tester.getTopLeft(find.text('November')); |
| expect(tester.getTopLeft(find.text('11')).dy, lastOffset.dy); |
| |
| lastOffset = tester.getTopLeft(find.text('11')); |
| expect(tester.getTopLeft(find.text('2022')).dy, lastOffset.dy); |
| }); |
| |
| testWidgets('CupertinoTimerPicker does not crash at zero area', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| CupertinoApp( |
| home: Center( |
| child: SizedBox.shrink(child: CupertinoTimerPicker(onTimerDurationChanged: (_) {})), |
| ), |
| ), |
| ); |
| expect(tester.getSize(find.byType(CupertinoTimerPicker)), Size.zero); |
| }); |
| } |
| |
| Widget _buildPicker({ |
| FixedExtentScrollController? controller, |
| required ValueChanged<int> onSelectedItemChanged, |
| }) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: CupertinoPicker( |
| scrollController: controller, |
| itemExtent: 100.0, |
| onSelectedItemChanged: onSelectedItemChanged, |
| children: List<Widget>.generate(100, (int index) { |
| return Center(child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString()))); |
| }), |
| ), |
| ); |
| } |
| |
| double getColumnWidth(String text, TextStyle textStyle, BuildContext context) { |
| return TextPainter.computeMaxIntrinsicWidth( |
| text: TextSpan(text: text, style: textStyle), |
| textDirection: Directionality.of(context), |
| ); |
| } |