| // 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 'dart:ui'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'feedback_tester.dart'; |
| |
| void main() { |
| late DateTime firstDate; |
| late DateTime lastDate; |
| late DateTime? currentDate; |
| late DateTimeRange? initialDateRange; |
| late DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar; |
| |
| String? cancelText; |
| String? confirmText; |
| String? errorInvalidRangeText; |
| String? errorFormatText; |
| String? errorInvalidText; |
| String? fieldStartHintText; |
| String? fieldEndHintText; |
| String? fieldStartLabelText; |
| String? fieldEndLabelText; |
| String? helpText; |
| String? saveText; |
| |
| setUp(() { |
| firstDate = DateTime(2015); |
| lastDate = DateTime(2016, DateTime.december, 31); |
| currentDate = null; |
| initialDateRange = DateTimeRange( |
| start: DateTime(2016, DateTime.january, 15), |
| end: DateTime(2016, DateTime.january, 25), |
| ); |
| initialEntryMode = DatePickerEntryMode.calendar; |
| |
| cancelText = null; |
| confirmText = null; |
| errorInvalidRangeText = null; |
| errorFormatText = null; |
| errorInvalidText = null; |
| fieldStartHintText = null; |
| fieldEndHintText = null; |
| fieldStartLabelText = null; |
| fieldEndLabelText = null; |
| helpText = null; |
| saveText = null; |
| }); |
| |
| Future<void> preparePicker( |
| WidgetTester tester, |
| Future<void> Function(Future<DateTimeRange?> date) callback, { |
| TextDirection textDirection = TextDirection.ltr, |
| bool useMaterial3 = false, |
| }) async { |
| late BuildContext buttonContext; |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData(useMaterial3: useMaterial3), |
| home: Material( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| onPressed: () { |
| buttonContext = context; |
| }, |
| child: const Text('Go'), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('Go')); |
| expect(buttonContext, isNotNull); |
| |
| final Future<DateTimeRange?> range = showDateRangePicker( |
| context: buttonContext, |
| initialDateRange: initialDateRange, |
| firstDate: firstDate, |
| lastDate: lastDate, |
| currentDate: currentDate, |
| initialEntryMode: initialEntryMode, |
| cancelText: cancelText, |
| confirmText: confirmText, |
| errorInvalidRangeText: errorInvalidRangeText, |
| errorFormatText: errorFormatText, |
| errorInvalidText: errorInvalidText, |
| fieldStartHintText: fieldStartHintText, |
| fieldEndHintText: fieldEndHintText, |
| fieldStartLabelText: fieldStartLabelText, |
| fieldEndLabelText: fieldEndLabelText, |
| helpText: helpText, |
| saveText: saveText, |
| builder: (BuildContext context, Widget? child) { |
| return Directionality( |
| textDirection: textDirection, |
| child: child ?? const SizedBox(), |
| ); |
| }, |
| ); |
| |
| await tester.pumpAndSettle(const Duration(seconds: 1)); |
| await callback(range); |
| } |
| |
| testWidgets('Save and help text is used', (WidgetTester tester) async { |
| helpText = 'help'; |
| saveText = 'make it so'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.text(helpText!), findsOneWidget); |
| expect(find.text(saveText!), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('Material3 has sentence case labels', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.text('Save'), findsOneWidget); |
| expect(find.text('Select range'), findsOneWidget); |
| }, useMaterial3: true); |
| }); |
| |
| testWidgets('Initial date is the default', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('SAVE')); |
| expect( |
| await range, |
| DateTimeRange( |
| start: DateTime(2016, DateTime.january, 15), |
| end: DateTime(2016, DateTime.january, 25), |
| ), |
| ); |
| }); |
| }); |
| |
| testWidgets('Last month header should be visible if last date is selected', (WidgetTester tester) async { |
| firstDate = DateTime(2015); |
| lastDate = DateTime(2016, DateTime.december, 31); |
| initialDateRange = DateTimeRange( |
| start: lastDate, |
| end: lastDate, |
| ); |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // December header should be showing, but no November |
| expect(find.text('December 2016'), findsOneWidget); |
| expect(find.text('November 2016'), findsNothing); |
| }); |
| }); |
| |
| testWidgets('First month header should be visible if first date is selected', (WidgetTester tester) async { |
| firstDate = DateTime(2015); |
| lastDate = DateTime(2016, DateTime.december, 31); |
| initialDateRange = DateTimeRange( |
| start: firstDate, |
| end: firstDate, |
| ); |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // January and February headers should be showing, but no March |
| expect(find.text('January 2015'), findsOneWidget); |
| expect(find.text('February 2015'), findsOneWidget); |
| expect(find.text('March 2015'), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Current month header should be visible if no date is selected', (WidgetTester tester) async { |
| firstDate = DateTime(2015); |
| lastDate = DateTime(2016, DateTime.december, 31); |
| currentDate = DateTime(2016, DateTime.september); |
| initialDateRange = null; |
| |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // September and October headers should be showing, but no August |
| expect(find.text('September 2016'), findsOneWidget); |
| expect(find.text('October 2016'), findsOneWidget); |
| expect(find.text('August 2016'), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Can cancel', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.byIcon(Icons.close)); |
| expect(await range, isNull); |
| }); |
| }); |
| |
| testWidgets('Can select a range', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('12').first); |
| await tester.tap(find.text('14').first); |
| await tester.tap(find.text('SAVE')); |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 12), |
| end: DateTime(2016, DateTime.january, 14), |
| )); |
| }); |
| }); |
| |
| testWidgets('Tapping earlier date resets selected range', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('12').first); |
| await tester.tap(find.text('11').first); |
| await tester.tap(find.text('15').first); |
| await tester.tap(find.text('SAVE')); |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 11), |
| end: DateTime(2016, DateTime.january, 15), |
| )); |
| }); |
| }); |
| |
| testWidgets('Can select single day range', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('12').first); |
| await tester.tap(find.text('12').first); |
| await tester.tap(find.text('SAVE')); |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 12), |
| end: DateTime(2016, DateTime.january, 12), |
| )); |
| }); |
| }); |
| |
| testWidgets('Cannot select a day outside bounds', (WidgetTester tester) async { |
| initialDateRange = DateTimeRange( |
| start: DateTime(2017, DateTime.january, 13), |
| end: DateTime(2017, DateTime.january, 15), |
| ); |
| firstDate = DateTime(2017, DateTime.january, 12); |
| lastDate = DateTime(2017, DateTime.january, 16); |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // Earlier than firstDate. Should be ignored. |
| await tester.tap(find.text('10')); |
| // Later than lastDate. Should be ignored. |
| await tester.tap(find.text('20')); |
| await tester.tap(find.text('SAVE')); |
| // We should still be on the initial date. |
| expect(await range, initialDateRange); |
| }); |
| }); |
| |
| testWidgets('Can switch from calendar to input entry mode', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNothing); |
| await tester.tap(find.byIcon(Icons.edit)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TextField), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('Can switch from input to calendar entry mode', (WidgetTester tester) async { |
| initialEntryMode = DatePickerEntryMode.input; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNWidgets(2)); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TextField), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Can not switch out of calendarOnly mode', (WidgetTester tester) async { |
| initialEntryMode = DatePickerEntryMode.calendarOnly; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNothing); |
| expect(find.byIcon(Icons.edit), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Can not switch out of inputOnly mode', (WidgetTester tester) async { |
| initialEntryMode = DatePickerEntryMode.inputOnly; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNWidgets(2)); |
| expect(find.byIcon(Icons.calendar_today), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Input only mode should validate date', (WidgetTester tester) async { |
| initialEntryMode = DatePickerEntryMode.inputOnly; |
| errorInvalidText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '08/08/2014'); |
| await tester.enterText(find.byType(TextField).at(1), '08/08/2014'); |
| expect(find.text(errorInvalidText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorInvalidText!), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('Switching to input mode keeps selected date', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('12').first); |
| await tester.tap(find.text('14').first); |
| await tester.tap(find.byIcon(Icons.edit)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('OK')); |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 12), |
| end: DateTime(2016, DateTime.january, 14), |
| )); |
| }); |
| }); |
| |
| group('Toggle from input entry mode validates dates', () { |
| setUp(() { |
| initialEntryMode = DatePickerEntryMode.input; |
| }); |
| |
| testWidgets('Invalid start date', (WidgetTester tester) async { |
| // Invalid start date should have neither a start nor end date selected in |
| // calendar mode |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/27/1918'); |
| await tester.enterText(find.byType(TextField).at(1), '12/25/2016'); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Start Date'), findsOneWidget); |
| expect(find.text('End Date'), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('Invalid end date', (WidgetTester tester) async { |
| // Invalid end date should only have a start date selected |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/24/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/25/2050'); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Dec 24'), findsOneWidget); |
| expect(find.text('End Date'), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('Invalid range', (WidgetTester tester) async { |
| // Start date after end date should just use the start date |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/25/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/24/2016'); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| |
| expect(find.text('Dec 25'), findsOneWidget); |
| expect(find.text('End Date'), findsOneWidget); |
| }); |
| }); |
| }); |
| |
| testWidgets('OK Cancel button layout', (WidgetTester tester) async { |
| Widget buildFrame(TextDirection textDirection) { |
| return MaterialApp( |
| home: Material( |
| child: Center( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| child: const Text('X'), |
| onPressed: () { |
| showDateRangePicker( |
| context: context, |
| firstDate:DateTime(2001), |
| lastDate: DateTime(2031, DateTime.december, 31), |
| builder: (BuildContext context, Widget? child) { |
| return Directionality( |
| textDirection: textDirection, |
| child: child ?? const SizedBox(), |
| ); |
| }, |
| ); |
| }, |
| ); |
| }, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> showOkCancelDialog(TextDirection textDirection) async { |
| await tester.pumpWidget(buildFrame(textDirection)); |
| await tester.tap(find.text('X')); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.byIcon(Icons.edit)); |
| await tester.pumpAndSettle(); |
| } |
| |
| Future<void> dismissOkCancelDialog() async { |
| await tester.tap(find.text('CANCEL')); |
| await tester.pumpAndSettle(); |
| } |
| |
| await showOkCancelDialog(TextDirection.ltr); |
| expect(tester.getBottomRight(find.text('OK')).dx, 622); |
| expect(tester.getBottomLeft(find.text('OK')).dx, 594); |
| expect(tester.getBottomRight(find.text('CANCEL')).dx, 560); |
| await dismissOkCancelDialog(); |
| |
| await showOkCancelDialog(TextDirection.rtl); |
| expect(tester.getBottomRight(find.text('OK')).dx, 206); |
| expect(tester.getBottomLeft(find.text('OK')).dx, 178); |
| expect(tester.getBottomRight(find.text('CANCEL')).dx, 324); |
| await dismissOkCancelDialog(); |
| }); |
| |
| group('Haptic feedback', () { |
| const Duration hapticFeedbackInterval = Duration(milliseconds: 10); |
| late FeedbackTester feedback; |
| |
| setUp(() { |
| feedback = FeedbackTester(); |
| initialDateRange = DateTimeRange( |
| start: DateTime(2017, DateTime.january, 15), |
| end: DateTime(2017, DateTime.january, 17), |
| ); |
| firstDate = DateTime(2017, DateTime.january, 10); |
| lastDate = DateTime(2018, DateTime.january, 20); |
| }); |
| |
| tearDown(() { |
| feedback.dispose(); |
| }); |
| |
| testWidgets('Selecting dates vibrates', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('10').first); |
| await tester.pump(hapticFeedbackInterval); |
| expect(feedback.hapticCount, 1); |
| await tester.tap(find.text('12').first); |
| await tester.pump(hapticFeedbackInterval); |
| expect(feedback.hapticCount, 2); |
| await tester.tap(find.text('14').first); |
| await tester.pump(hapticFeedbackInterval); |
| expect(feedback.hapticCount, 3); |
| }); |
| }); |
| |
| testWidgets('Tapping unselectable date does not vibrate', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('8').first); |
| await tester.pump(hapticFeedbackInterval); |
| expect(feedback.hapticCount, 0); |
| }); |
| }); |
| }); |
| |
| group('Keyboard navigation', () { |
| testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNothing); |
| // Navigate to the entry toggle button and activate it |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| // Should be in the input mode |
| expect(find.byType(TextField), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('Can navigate date grid with arrow keys', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // Navigate to the grid |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pumpAndSettle(); |
| |
| // Navigate from Jan 15 to Jan 18 with arrow keys |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| |
| // Activate it to select the beginning of the range |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate to Jan 29 |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| |
| // Activate it to select the end of the range |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate out of the grid and to the OK button |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| |
| // Activate OK |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Should have selected Jan 18 - Jan 29 |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 18), |
| end: DateTime(2016, DateTime.january, 29), |
| )); |
| }); |
| }); |
| |
| testWidgets('Navigating with arrow keys scrolls as needed', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // Jan and Feb headers should be showing, but no March |
| expect(find.text('January 2016'), findsOneWidget); |
| expect(find.text('February 2016'), findsOneWidget); |
| expect(find.text('March 2016'), findsNothing); |
| |
| // Navigate to the grid |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| |
| // Navigate from Jan 15 to Jan 18 with arrow keys |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pumpAndSettle(); |
| |
| // Activate it to select the beginning of the range |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate to Mar 17 |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pumpAndSettle(); |
| |
| // Jan should have scrolled off, Mar should be visible |
| expect(find.text('January 2016'), findsNothing); |
| expect(find.text('February 2016'), findsOneWidget); |
| expect(find.text('March 2016'), findsOneWidget); |
| |
| // Activate it to select the end of the range |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate out of the grid and to the OK button |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| |
| // Activate OK |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Should have selected Jan 18 - Mar 17 |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 18), |
| end: DateTime(2016, DateTime.march, 17), |
| )); |
| }); |
| }); |
| |
| testWidgets('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| // Navigate to the grid |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| |
| // Navigate from Jan 15 to 19 with arrow keys |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| |
| // Activate it |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate to Jan 21 |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pumpAndSettle(); |
| |
| // Activate it |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Navigate out of the grid and to the OK button |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| |
| // Activate OK |
| await tester.sendKeyEvent(LogicalKeyboardKey.space); |
| await tester.pumpAndSettle(); |
| |
| // Should have selected Jan 19 - Mar 21 |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.january, 19), |
| end: DateTime(2016, DateTime.january, 21), |
| )); |
| }, textDirection: TextDirection.rtl); |
| }); |
| }); |
| |
| group('Input mode', () { |
| setUp(() { |
| firstDate = DateTime(2015); |
| lastDate = DateTime(2017, DateTime.december, 31); |
| initialDateRange = DateTimeRange( |
| start: DateTime(2017, DateTime.january, 15), |
| end: DateTime(2017, DateTime.january, 17), |
| ); |
| initialEntryMode = DatePickerEntryMode.input; |
| }); |
| |
| testWidgets('Initial entry mode is used', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('All custom strings are used', (WidgetTester tester) async { |
| initialDateRange = null; |
| cancelText = 'nope'; |
| confirmText = 'yep'; |
| fieldStartHintText = 'hint1'; |
| fieldEndHintText = 'hint2'; |
| fieldStartLabelText = 'label1'; |
| fieldEndLabelText = 'label2'; |
| helpText = 'help'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.text(cancelText!), findsOneWidget); |
| expect(find.text(confirmText!), findsOneWidget); |
| expect(find.text(fieldStartHintText!), findsOneWidget); |
| expect(find.text(fieldEndHintText!), findsOneWidget); |
| expect(find.text(fieldStartLabelText!), findsOneWidget); |
| expect(find.text(fieldEndLabelText!), findsOneWidget); |
| expect(find.text(helpText!), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('Initial date is the default', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.tap(find.text('OK')); |
| expect(await range, DateTimeRange( |
| start: DateTime(2017, DateTime.january, 15), |
| end: DateTime(2017, DateTime.january, 17), |
| )); |
| }); |
| }); |
| |
| testWidgets('Can toggle to calendar entry mode', (WidgetTester tester) async { |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect(find.byType(TextField), findsNWidgets(2)); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| expect(find.byType(TextField), findsNothing); |
| }); |
| }); |
| |
| testWidgets('Toggle to calendar mode keeps selected date', (WidgetTester tester) async { |
| initialDateRange = null; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/25/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/27/2016'); |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('SAVE')); |
| |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.december, 25), |
| end: DateTime(2016, DateTime.december, 27), |
| )); |
| }); |
| }); |
| |
| testWidgets('Entered text returns range', (WidgetTester tester) async { |
| initialDateRange = null; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/25/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/27/2016'); |
| await tester.tap(find.text('OK')); |
| |
| expect(await range, DateTimeRange( |
| start: DateTime(2016, DateTime.december, 25), |
| end: DateTime(2016, DateTime.december, 27), |
| )); |
| }); |
| }); |
| |
| testWidgets('Too short entered text shows error', (WidgetTester tester) async { |
| initialDateRange = null; |
| errorFormatText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/25'); |
| await tester.enterText(find.byType(TextField).at(1), '12/25'); |
| expect(find.text(errorFormatText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorFormatText!), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('Bad format entered text shows error', (WidgetTester tester) async { |
| initialDateRange = null; |
| errorFormatText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '20202014'); |
| await tester.enterText(find.byType(TextField).at(1), '20212014'); |
| expect(find.text(errorFormatText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorFormatText!), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('Invalid entered text shows error', (WidgetTester tester) async { |
| initialDateRange = null; |
| errorInvalidText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '08/08/2014'); |
| await tester.enterText(find.byType(TextField).at(1), '08/08/2014'); |
| expect(find.text(errorInvalidText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorInvalidText!), findsNWidgets(2)); |
| }); |
| }); |
| |
| testWidgets('End before start date shows error', (WidgetTester tester) async { |
| initialDateRange = null; |
| errorInvalidRangeText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/27/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/25/2016'); |
| expect(find.text(errorInvalidRangeText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorInvalidRangeText!), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('Error text only displayed for invalid date', (WidgetTester tester) async { |
| initialDateRange = null; |
| errorInvalidText = 'oops'; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/27/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '01/01/2018'); |
| expect(find.text(errorInvalidText!), findsNothing); |
| |
| await tester.tap(find.text('OK')); |
| await tester.pumpAndSettle(); |
| expect(find.text(errorInvalidText!), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('End before start date does not get passed to calendar mode', (WidgetTester tester) async { |
| initialDateRange = null; |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| await tester.enterText(find.byType(TextField).at(0), '12/27/2016'); |
| await tester.enterText(find.byType(TextField).at(1), '12/25/2016'); |
| |
| await tester.tap(find.byIcon(Icons.calendar_today)); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('SAVE')); |
| await tester.pumpAndSettle(); |
| |
| // Save button should be disabled, so dialog should still be up |
| // with the first date selected, but no end date |
| expect(find.text('Dec 27'), findsOneWidget); |
| expect(find.text('End Date'), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async { |
| |
| // Given a custom paint for an input decoration, extract the border and |
| // fill color and test them against the expected values. |
| void testInputDecorator(CustomPaint decoratorPaint, InputBorder expectedBorder, Color expectedContainerColor) { |
| final dynamic/*_InputBorderPainter*/ inputBorderPainter = decoratorPaint.foregroundPainter; |
| // ignore: avoid_dynamic_calls |
| final dynamic/*_InputBorderTween*/ inputBorderTween = inputBorderPainter.border; |
| // ignore: avoid_dynamic_calls |
| final Animation<double> animation = inputBorderPainter.borderAnimation as Animation<double>; |
| // ignore: avoid_dynamic_calls |
| final InputBorder actualBorder = inputBorderTween.evaluate(animation) as InputBorder; |
| // ignore: avoid_dynamic_calls |
| final Color containerColor = inputBorderPainter.blendedColor as Color; |
| |
| expect(actualBorder, equals(expectedBorder)); |
| expect(containerColor, equals(expectedContainerColor)); |
| } |
| |
| late BuildContext buttonContext; |
| const InputBorder border = InputBorder.none; |
| await tester.pumpWidget(MaterialApp( |
| theme: ThemeData.light().copyWith( |
| inputDecorationTheme: const InputDecorationTheme( |
| border: border, |
| ), |
| ), |
| home: Material( |
| child: Builder( |
| builder: (BuildContext context) { |
| return ElevatedButton( |
| onPressed: () { |
| buttonContext = context; |
| }, |
| child: const Text('Go'), |
| ); |
| }, |
| ), |
| ), |
| )); |
| |
| await tester.tap(find.text('Go')); |
| expect(buttonContext, isNotNull); |
| |
| showDateRangePicker( |
| context: buttonContext, |
| initialDateRange: initialDateRange, |
| firstDate: firstDate, |
| lastDate: lastDate, |
| initialEntryMode: DatePickerEntryMode.input, |
| ); |
| await tester.pumpAndSettle(); |
| |
| final Finder borderContainers = find.descendant( |
| of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'), |
| matching: find.byWidgetPredicate((Widget w) => w is CustomPaint), |
| ); |
| |
| // Test the start date text field |
| testInputDecorator(tester.widget(borderContainers.first), border, Colors.transparent); |
| |
| // Test the end date text field |
| testInputDecorator(tester.widget(borderContainers.last), border, Colors.transparent); |
| }); |
| }); |
| |
| testWidgets('DatePickerDialog is state restorable', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| restorationScopeId: 'app', |
| home: _RestorableDateRangePickerDialogTestWidget(), |
| ), |
| ); |
| |
| // The date range picker should be closed. |
| expect(find.byType(DateRangePickerDialog), findsNothing); |
| expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); |
| |
| // Open the date range picker. |
| await tester.tap(find.text('X')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| |
| final TestRestorationData restorationData = await tester.getRestorationData(); |
| await tester.restartAndRestore(); |
| |
| // The date range picker should be open after restoring. |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| |
| // Close the date range picker. |
| await tester.tap(find.byIcon(Icons.close)); |
| await tester.pumpAndSettle(); |
| |
| // The date range picker should be closed, the text value updated to the |
| // newly selected date. |
| expect(find.byType(DateRangePickerDialog), findsNothing); |
| expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); |
| |
| // The date range picker should be open after restoring. |
| await tester.restoreFrom(restorationData); |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| |
| // // Select a different date and close the date range picker. |
| await tester.tap(find.text('12').first); |
| await tester.pumpAndSettle(); |
| await tester.tap(find.text('14').first); |
| await tester.pumpAndSettle(); |
| |
| // Restart after the new selection. It should remain selected. |
| await tester.restartAndRestore(); |
| |
| // Close the date range picker. |
| await tester.tap(find.text('SAVE')); |
| await tester.pumpAndSettle(); |
| |
| // The date range picker should be closed, the text value updated to the |
| // newly selected date. |
| expect(find.byType(DateRangePickerDialog), findsNothing); |
| expect(find.text('12/1/2021 to 14/1/2021'), findsOneWidget); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 |
| |
| testWidgets('DateRangePickerDialog state restoration - DatePickerEntryMode', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp( |
| restorationScopeId: 'app', |
| home: _RestorableDateRangePickerDialogTestWidget( |
| datePickerEntryMode: DatePickerEntryMode.calendarOnly, |
| ), |
| ), |
| ); |
| |
| // The date range picker should be closed. |
| expect(find.byType(DateRangePickerDialog), findsNothing); |
| expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); |
| |
| // Open the date range picker. |
| await tester.tap(find.text('X')); |
| await tester.pumpAndSettle(); |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| |
| // Only in calendar mode and cannot switch out. |
| expect(find.byType(TextField), findsNothing); |
| expect(find.byIcon(Icons.edit), findsNothing); |
| |
| final TestRestorationData restorationData = await tester.getRestorationData(); |
| await tester.restartAndRestore(); |
| |
| // The date range picker should be open after restoring. |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| // Only in calendar mode and cannot switch out. |
| expect(find.byType(TextField), findsNothing); |
| expect(find.byIcon(Icons.edit), findsNothing); |
| |
| // Tap on the barrier. |
| await tester.tap(find.byIcon(Icons.close)); |
| await tester.pumpAndSettle(); |
| |
| // The date range picker should be closed, the text value should be the same |
| // as before. |
| expect(find.byType(DateRangePickerDialog), findsNothing); |
| expect(find.text('1/1/2021 to 5/1/2021'), findsOneWidget); |
| |
| // The date range picker should be open after restoring. |
| await tester.restoreFrom(restorationData); |
| expect(find.byType(DateRangePickerDialog), findsOneWidget); |
| // Only in calendar mode and cannot switch out. |
| expect(find.byType(TextField), findsNothing); |
| expect(find.byIcon(Icons.edit), findsNothing); |
| }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 |
| |
| group('showDateRangePicker avoids overlapping display features', () { |
| testWidgets('positioning with anchorPoint', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: child!, |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showDateRangePicker( |
| context: context, |
| firstDate: DateTime(2018), |
| lastDate: DateTime(2030), |
| anchorPoint: const Offset(1000, 0), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // Should take the right side of the screen |
| expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), const Offset(410.0, 0.0)); |
| expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0)); |
| }); |
| |
| testWidgets('positioning with Directionality', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: Directionality( |
| textDirection: TextDirection.rtl, |
| child: child!, |
| ), |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showDateRangePicker( |
| context: context, |
| firstDate: DateTime(2018), |
| lastDate: DateTime(2030), |
| anchorPoint: const Offset(1000, 0), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // By default it should place the dialog on the right screen |
| expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), const Offset(410.0, 0.0)); |
| expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(800.0, 600.0)); |
| }); |
| |
| testWidgets('positioning with defaults', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| builder: (BuildContext context, Widget? child) { |
| return MediaQuery( |
| // Display has a vertical hinge down the middle |
| data: const MediaQueryData( |
| size: Size(800, 600), |
| displayFeatures: <DisplayFeature>[ |
| DisplayFeature( |
| bounds: Rect.fromLTRB(390, 0, 410, 600), |
| type: DisplayFeatureType.hinge, |
| state: DisplayFeatureState.unknown, |
| ), |
| ], |
| ), |
| child: child!, |
| ); |
| }, |
| home: const Center(child: Text('Test')), |
| ), |
| ); |
| |
| final BuildContext context = tester.element(find.text('Test')); |
| showDateRangePicker( |
| context: context, |
| firstDate: DateTime(2018), |
| lastDate: DateTime(2030), |
| ); |
| await tester.pumpAndSettle(); |
| |
| // By default it should place the dialog on the left screen |
| expect(tester.getTopLeft(find.byType(DateRangePickerDialog)), Offset.zero); |
| expect(tester.getBottomRight(find.byType(DateRangePickerDialog)), const Offset(390.0, 600.0)); |
| }); |
| }); |
| |
| group('Semantics', () { |
| testWidgets('calendar mode', (WidgetTester tester) async { |
| final SemanticsHandle semantics = tester.ensureSemantics(); |
| currentDate = DateTime(2016, DateTime.january, 30); |
| addTearDown(semantics.dispose); |
| await preparePicker(tester, (Future<DateTimeRange?> range) async { |
| expect( |
| tester.getSemantics(find.text('30')), |
| matchesSemantics( |
| label: '30, Saturday, January 30, 2016, Today', |
| hasTapAction: true, |
| isFocusable: true, |
| ), |
| ); |
| }); |
| }); |
| }); |
| } |
| |
| class _RestorableDateRangePickerDialogTestWidget extends StatefulWidget { |
| const _RestorableDateRangePickerDialogTestWidget({ |
| this.datePickerEntryMode = DatePickerEntryMode.calendar, |
| }); |
| |
| final DatePickerEntryMode datePickerEntryMode; |
| |
| @override |
| _RestorableDateRangePickerDialogTestWidgetState createState() => _RestorableDateRangePickerDialogTestWidgetState(); |
| } |
| |
| class _RestorableDateRangePickerDialogTestWidgetState extends State<_RestorableDateRangePickerDialogTestWidget> with RestorationMixin { |
| @override |
| String? get restorationId => 'scaffold_state'; |
| |
| final RestorableDateTimeN _startDate = RestorableDateTimeN(DateTime(2021)); |
| final RestorableDateTimeN _endDate = RestorableDateTimeN(DateTime(2021, 1, 5)); |
| late final RestorableRouteFuture<DateTimeRange?> _restorableDateRangePickerRouteFuture = RestorableRouteFuture<DateTimeRange?>( |
| onComplete: _selectDateRange, |
| onPresent: (NavigatorState navigator, Object? arguments) { |
| return navigator.restorablePush( |
| _dateRangePickerRoute, |
| arguments: <String, dynamic>{ |
| 'datePickerEntryMode': widget.datePickerEntryMode.index, |
| }, |
| ); |
| }, |
| ); |
| |
| @override |
| void restoreState(RestorationBucket? oldBucket, bool initialRestore) { |
| registerForRestoration(_startDate, 'start_date'); |
| registerForRestoration(_endDate, 'end_date'); |
| registerForRestoration(_restorableDateRangePickerRouteFuture, 'date_picker_route_future'); |
| } |
| |
| void _selectDateRange(DateTimeRange? newSelectedDate) { |
| if (newSelectedDate != null) { |
| setState(() { |
| _startDate.value = newSelectedDate.start; |
| _endDate.value = newSelectedDate.end; |
| }); |
| } |
| } |
| |
| static Route<DateTimeRange?> _dateRangePickerRoute( |
| BuildContext context, |
| Object? arguments, |
| ) { |
| return DialogRoute<DateTimeRange?>( |
| context: context, |
| builder: (BuildContext context) { |
| final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>; |
| return DateRangePickerDialog( |
| restorationId: 'date_picker_dialog', |
| initialEntryMode: DatePickerEntryMode.values[args['datePickerEntryMode'] as int], |
| firstDate: DateTime(2021), |
| currentDate: DateTime(2021, 1, 25), |
| lastDate: DateTime(2022), |
| ); |
| }, |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final DateTime? startDateTime = _startDate.value; |
| final DateTime? endDateTime = _endDate.value; |
| // Example: "25/7/1994" |
| final String startDateTimeString = '${startDateTime?.day}/${startDateTime?.month}/${startDateTime?.year}'; |
| final String endDateTimeString = '${endDateTime?.day}/${endDateTime?.month}/${endDateTime?.year}'; |
| return Scaffold( |
| body: Center( |
| child: Column( |
| children: <Widget>[ |
| OutlinedButton( |
| onPressed: () { |
| _restorableDateRangePickerRouteFuture.present(); |
| }, |
| child: const Text('X'), |
| ), |
| Text('$startDateTimeString to $endDateTimeString'), |
| ], |
| ), |
| ), |
| ); |
| } |
| } |