// 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.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('Default layout (calendar mode)', (WidgetTester tester) async { await preparePicker(tester, (Future<DateTimeRange?> range) async { final Finder helpText = find.text('Select range'); final Finder firstDateHeaderText = find.text('Jan 15'); final Finder lastDateHeaderText = find.text('Jan 25, 2016'); final Finder saveText = find.text('Save'); expect(helpText, findsOneWidget); expect(firstDateHeaderText, findsOneWidget); expect(lastDateHeaderText, findsOneWidget); expect(saveText, findsOneWidget); // Test the close button position. final Offset closeButtonBottomRight = tester.getBottomRight(find.byType(CloseButton)); final Offset helpTextTopLeft = tester.getTopLeft(helpText); expect(closeButtonBottomRight.dx, 56.0); expect(closeButtonBottomRight.dy, helpTextTopLeft.dy); // Test the save and entry buttons position. final Offset saveButtonBottomLeft = tester.getBottomLeft(find.byType(TextButton)); final Offset entryButtonBottomLeft = tester.getBottomLeft( find.widgetWithIcon(IconButton, Icons.edit_outlined), ); expect(saveButtonBottomLeft.dx, moreOrLessEquals(711.6, epsilon: 1e-5)); if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 expect(saveButtonBottomLeft.dy, helpTextTopLeft.dy); } expect(entryButtonBottomLeft.dx, saveButtonBottomLeft.dx - 48.0); if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 expect(entryButtonBottomLeft.dy, helpTextTopLeft.dy); } // Test help text position. final Offset helpTextBottomLeft = tester.getBottomLeft(helpText); expect(helpTextBottomLeft.dx, 72.0); if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 expect(helpTextBottomLeft.dy, closeButtonBottomRight.dy + 20.0); } // Test the header position. final Offset firstDateHeaderTopLeft = tester.getTopLeft(firstDateHeaderText); final Offset lastDateHeaderTopLeft = tester.getTopLeft(lastDateHeaderText); expect(firstDateHeaderTopLeft.dx, 72.0); expect(firstDateHeaderTopLeft.dy, helpTextBottomLeft.dy + 8.0); final Offset firstDateHeaderTopRight = tester.getTopRight(firstDateHeaderText); expect(lastDateHeaderTopLeft.dx, firstDateHeaderTopRight.dx + 66.0); expect(lastDateHeaderTopLeft.dy, helpTextBottomLeft.dy + 8.0); // Test the day headers position. final Offset dayHeadersGridTopLeft = tester.getTopLeft(find.byType(GridView).first); final Offset firstDateHeaderBottomLeft = tester.getBottomLeft(firstDateHeaderText); expect(dayHeadersGridTopLeft.dx, (800 - 384) / 2); expect(dayHeadersGridTopLeft.dy, firstDateHeaderBottomLeft.dy + 16.0); // Test the calendar custom scroll view position. final Offset calendarScrollViewTopLeft = tester.getTopLeft(find.byType(CustomScrollView)); final Offset dayHeadersGridBottomLeft = tester.getBottomLeft(find.byType(GridView).first); expect(calendarScrollViewTopLeft.dx, 0.0); expect(calendarScrollViewTopLeft.dy, dayHeadersGridBottomLeft.dy); }, useMaterial3: true); }); testWidgets('Default Dialog properties (calendar mode)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Material dialogMaterial = tester.widget<Material>( find.descendant(of: find.byType(Dialog), matching: find.byType(Material), ).first); expect(dialogMaterial.color, theme.colorScheme.surface); expect(dialogMaterial.shadowColor, Colors.transparent); expect(dialogMaterial.surfaceTintColor, Colors.transparent); expect(dialogMaterial.elevation, 0.0); expect(dialogMaterial.shape, const RoundedRectangleBorder()); expect(dialogMaterial.clipBehavior, Clip.antiAlias); final Dialog dialog = tester.widget<Dialog>(find.byType(Dialog)); expect(dialog.insetPadding, EdgeInsets.zero); }, useMaterial3: theme.useMaterial3); }); testWidgets('Default Dialog properties (input mode)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Material dialogMaterial = tester.widget<Material>( find.descendant(of: find.byType(Dialog), matching: find.byType(Material), ).first); expect(dialogMaterial.color, theme.colorScheme.surface); expect(dialogMaterial.shadowColor, Colors.transparent); expect(dialogMaterial.surfaceTintColor, Colors.transparent); expect(dialogMaterial.elevation, 0.0); expect(dialogMaterial.shape, const RoundedRectangleBorder()); expect(dialogMaterial.clipBehavior, Clip.antiAlias); final Dialog dialog = tester.widget<Dialog>(find.byType(Dialog)); expect(dialog.insetPadding, EdgeInsets.zero); }, useMaterial3: theme.useMaterial3); }); testWidgets('Scaffold and AppBar defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Scaffold scaffold = tester.widget<Scaffold>(find.byType(Scaffold)); expect(scaffold.backgroundColor, null); final AppBar appBar = tester.widget<AppBar>(find.byType(AppBar)); final IconThemeData iconTheme = IconThemeData(color: theme.colorScheme.onSurfaceVariant); expect(appBar.iconTheme, iconTheme); expect(appBar.actionsIconTheme, iconTheme); expect(appBar.elevation, 0); expect(appBar.scrolledUnderElevation, 0); expect(appBar.backgroundColor, Colors.transparent); }, useMaterial3: theme.useMaterial3); }); group('Landscape input-only date picker headers use headlineSmall', () { // Regression test for https://github.com/flutter/flutter/issues/122056 // Common screen size roughly based on a Pixel 1 const Size kCommonScreenSizePortrait = Size(1070, 1770); const Size kCommonScreenSizeLandscape = Size(1770, 1070); Future<void> showPicker(WidgetTester tester, Size size) async { addTearDown(tester.view.reset); tester.view.physicalSize = size; tester.view.devicePixelRatio = 1.0; initialEntryMode = DatePickerEntryMode.input; await preparePicker(tester, (Future<DateTimeRange?> range) async { }, useMaterial3: true); } testWidgets('portrait', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizePortrait); expect(tester.widget<Text>(find.text('Jan 15 – Jan 25, 2016')).style?.fontSize, 32); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); }); testWidgetsWithLeakTracking('landscape', (WidgetTester tester) async { await showPicker(tester, kCommonScreenSizeLandscape); expect(tester.widget<Text>(find.text('Jan 15 – Jan 25, 2016')).style?.fontSize, 24); await tester.tap(find.text('Cancel')); await tester.pumpAndSettle(); }, // TODO(polina-c): remove after resolving // https://github.com/flutter/flutter/issues/130354 leakTrackingTestConfig: const LeakTrackingTestConfig( allowAllNotGCed: true, allowAllNotDisposed: true, )); }); 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( theme: ThemeData(useMaterial3: false), 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('Default Dialog properties (input mode)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: true); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Material dialogMaterial = tester.widget<Material>( find.descendant(of: find.byType(Dialog), matching: find.byType(Material), ).first); expect(dialogMaterial.color, theme.colorScheme.surface); expect(dialogMaterial.shadowColor, Colors.transparent); expect(dialogMaterial.surfaceTintColor, theme.colorScheme.surfaceTint); expect(dialogMaterial.elevation, 6.0); expect( dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(28.0))), ); expect(dialogMaterial.clipBehavior, Clip.antiAlias); final Dialog dialog = tester.widget<Dialog>(find.byType(Dialog)); expect(dialog.insetPadding, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0)); }, useMaterial3: theme.useMaterial3); }); testWidgets('Default InputDecoration', (WidgetTester tester) async { await preparePicker(tester, (Future<DateTimeRange?> range) async { final InputDecoration startDateDecoration = tester.widget<TextField>( find.byType(TextField).first).decoration!; expect(startDateDecoration.border, const OutlineInputBorder()); expect(startDateDecoration.filled, false); expect(startDateDecoration.hintText, 'mm/dd/yyyy'); expect(startDateDecoration.labelText, 'Start Date'); expect(startDateDecoration.errorText, null); final InputDecoration endDateDecoration = tester.widget<TextField>( find.byType(TextField).last).decoration!; expect(endDateDecoration.border, const OutlineInputBorder()); expect(endDateDecoration.filled, false); expect(endDateDecoration.hintText, 'mm/dd/yyyy'); expect(endDateDecoration.labelText, 'End Date'); expect(endDateDecoration.errorText, null); }, useMaterial3: true); }); 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( MaterialApp( theme: ThemeData(useMaterial3: false), restorationScopeId: 'app', home: const _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); 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, ), ); }); semantics.dispose(); }); }); for (final TextInputType? keyboardType in <TextInputType?>[null, TextInputType.emailAddress]) { testWidgets('DateRangePicker takes keyboardType $keyboardType', (WidgetTester tester) async { 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); if (keyboardType == null) { // If no keyboardType, expect the default. showDateRangePicker( context: buttonContext, initialDateRange: initialDateRange, firstDate: firstDate, lastDate: lastDate, initialEntryMode: DatePickerEntryMode.input, ); } else { // If there is a keyboardType, expect it to be passed through. showDateRangePicker( context: buttonContext, initialDateRange: initialDateRange, firstDate: firstDate, lastDate: lastDate, initialEntryMode: DatePickerEntryMode.input, keyboardType: keyboardType, ); } await tester.pumpAndSettle(); final DateRangePickerDialog picker = tester.widget(find.byType(DateRangePickerDialog)); expect(picker.keyboardType, keyboardType ?? TextInputType.datetime); }); } testWidgets('honors switchToInputEntryModeIcon', (WidgetTester tester) async { Widget buildApp({bool? useMaterial3, Icon? switchToInputEntryModeIcon}) { return MaterialApp( theme: ThemeData( useMaterial3: useMaterial3 ?? false, ), home: Material( child: Builder( builder: (BuildContext context) { return ElevatedButton( child: const Text('Click X'), onPressed: () { showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime(2030), switchToInputEntryModeIcon: switchToInputEntryModeIcon, ); }, ); }, ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.edit), findsOneWidget); await tester.tap(find.byIcon(Icons.close)); await tester.pumpAndSettle(); await tester.pumpWidget(buildApp(useMaterial3: true)); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.edit_outlined), findsOneWidget); await tester.tap(find.byIcon(Icons.close)); await tester.pumpAndSettle(); await tester.pumpWidget( buildApp( switchToInputEntryModeIcon: const Icon(Icons.keyboard), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.keyboard), findsOneWidget); await tester.tap(find.byIcon(Icons.close)); await tester.pumpAndSettle(); }); testWidgets('honors switchToCalendarEntryModeIcon', (WidgetTester tester) async { Widget buildApp({bool? useMaterial3, Icon? switchToCalendarEntryModeIcon}) { return MaterialApp( theme: ThemeData( useMaterial3: useMaterial3 ?? false, ), home: Material( child: Builder( builder: (BuildContext context) { return ElevatedButton( child: const Text('Click X'), onPressed: () { showDateRangePicker( context: context, firstDate: DateTime(2020), lastDate: DateTime(2030), switchToCalendarEntryModeIcon: switchToCalendarEntryModeIcon, initialEntryMode: DatePickerEntryMode.input, cancelText: 'CANCEL', ); }, ); }, ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.calendar_today), findsOneWidget); await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); await tester.pumpWidget(buildApp(useMaterial3: true)); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.calendar_today), findsOneWidget); await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); await tester.pumpWidget( buildApp( switchToCalendarEntryModeIcon: const Icon(Icons.favorite), ), ); await tester.pumpAndSettle(); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byIcon(Icons.favorite), findsOneWidget); await tester.tap(find.text('CANCEL')); await tester.pumpAndSettle(); }); group('Material 2', () { // These tests are only relevant for Material 2. Once Material 2 // support is deprecated and the APIs are removed, these tests // can be deleted. testWidgets('Default layout (calendar mode)', (WidgetTester tester) async { await preparePicker(tester, (Future<DateTimeRange?> range) async { final Finder helpText = find.text('SELECT RANGE'); final Finder firstDateHeaderText = find.text('Jan 15'); final Finder lastDateHeaderText = find.text('Jan 25, 2016'); final Finder saveText = find.text('SAVE'); expect(helpText, findsOneWidget); expect(firstDateHeaderText, findsOneWidget); expect(lastDateHeaderText, findsOneWidget); expect(saveText, findsOneWidget); // Test the close button position. final Offset closeButtonBottomRight = tester.getBottomRight(find.byType(CloseButton)); final Offset helpTextTopLeft = tester.getTopLeft(helpText); expect(closeButtonBottomRight.dx, 56.0); expect(closeButtonBottomRight.dy, helpTextTopLeft.dy - 6.0); // Test the save and entry buttons position. final Offset saveButtonBottomLeft = tester.getBottomLeft(find.byType(TextButton)); final Offset entryButtonBottomLeft = tester.getBottomLeft( find.widgetWithIcon(IconButton, Icons.edit), ); expect(saveButtonBottomLeft.dx, 800 - 80.0); expect(saveButtonBottomLeft.dy, helpTextTopLeft.dy - 6.0); expect(entryButtonBottomLeft.dx, saveButtonBottomLeft.dx - 48.0); expect(entryButtonBottomLeft.dy, helpTextTopLeft.dy - 6.0); // Test help text position. final Offset helpTextBottomLeft = tester.getBottomLeft(helpText); expect(helpTextBottomLeft.dx, 72.0); expect(helpTextBottomLeft.dy, closeButtonBottomRight.dy + 16.0); // Test the header position. final Offset firstDateHeaderTopLeft = tester.getTopLeft(firstDateHeaderText); final Offset lastDateHeaderTopLeft = tester.getTopLeft(lastDateHeaderText); expect(firstDateHeaderTopLeft.dx, 72.0); expect(firstDateHeaderTopLeft.dy, helpTextBottomLeft.dy + 8.0); final Offset firstDateHeaderTopRight = tester.getTopRight(firstDateHeaderText); expect(lastDateHeaderTopLeft.dx, firstDateHeaderTopRight.dx + 72.0); expect(lastDateHeaderTopLeft.dy, helpTextBottomLeft.dy + 8.0); // Test the day headers position. final Offset dayHeadersGridTopLeft = tester.getTopLeft(find.byType(GridView).first); final Offset firstDateHeaderBottomLeft = tester.getBottomLeft(firstDateHeaderText); expect(dayHeadersGridTopLeft.dx, (800 - 384) / 2); expect(dayHeadersGridTopLeft.dy, firstDateHeaderBottomLeft.dy + 16.0); // Test the calendar custom scroll view position. final Offset calendarScrollViewTopLeft = tester.getTopLeft(find.byType(CustomScrollView)); final Offset dayHeadersGridBottomLeft = tester.getBottomLeft(find.byType(GridView).first); expect(calendarScrollViewTopLeft.dx, 0.0); expect(calendarScrollViewTopLeft.dy, dayHeadersGridBottomLeft.dy); }); }); testWidgets('Default Dialog properties (calendar mode)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Material dialogMaterial = tester.widget<Material>( find.descendant(of: find.byType(Dialog), matching: find.byType(Material), ).first); expect(dialogMaterial.color, theme.colorScheme.surface); expect(dialogMaterial.shadowColor, Colors.transparent); expect(dialogMaterial.surfaceTintColor, Colors.transparent); expect(dialogMaterial.elevation, 0.0); expect(dialogMaterial.shape, const RoundedRectangleBorder()); expect(dialogMaterial.clipBehavior, Clip.antiAlias); final Dialog dialog = tester.widget<Dialog>(find.byType(Dialog)); expect(dialog.insetPadding, EdgeInsets.zero); }); }); testWidgets('Scaffold and AppBar defaults', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Scaffold scaffold = tester.widget<Scaffold>(find.byType(Scaffold)); expect(scaffold.backgroundColor, theme.colorScheme.surface); final AppBar appBar = tester.widget<AppBar>(find.byType(AppBar)); final IconThemeData iconTheme = IconThemeData(color: theme.colorScheme.onPrimary); expect(appBar.iconTheme, iconTheme); expect(appBar.actionsIconTheme, iconTheme); expect(appBar.elevation, null); expect(appBar.scrolledUnderElevation, null); expect(appBar.backgroundColor, null); }); }); 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('Default Dialog properties (input mode)', (WidgetTester tester) async { final ThemeData theme = ThemeData(useMaterial3: false); await preparePicker(tester, (Future<DateTimeRange?> range) async { final Material dialogMaterial = tester.widget<Material>( find.descendant(of: find.byType(Dialog), matching: find.byType(Material), ).first); expect(dialogMaterial.color, theme.colorScheme.surface); expect(dialogMaterial.shadowColor, theme.shadowColor); expect(dialogMaterial.surfaceTintColor, null); expect(dialogMaterial.elevation, 24.0); expect( dialogMaterial.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), ); expect(dialogMaterial.clipBehavior, Clip.antiAlias); final Dialog dialog = tester.widget<Dialog>(find.byType(Dialog)); expect(dialog.insetPadding, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0)); }); }); testWidgets('Default InputDecoration', (WidgetTester tester) async { await preparePicker(tester, (Future<DateTimeRange?> range) async { final InputDecoration startDateDecoration = tester.widget<TextField>( find.byType(TextField).first).decoration!; expect(startDateDecoration.border, const UnderlineInputBorder()); expect(startDateDecoration.filled, false); expect(startDateDecoration.hintText, 'mm/dd/yyyy'); expect(startDateDecoration.labelText, 'Start Date'); expect(startDateDecoration.errorText, null); final InputDecoration endDateDecoration = tester.widget<TextField>( find.byType(TextField).last).decoration!; expect(endDateDecoration.border, const UnderlineInputBorder()); expect(endDateDecoration.filled, false); expect(endDateDecoration.hintText, 'mm/dd/yyyy'); expect(endDateDecoration.labelText, 'End Date'); expect(endDateDecoration.errorText, null); }); }); }); }); } 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; }); } } @pragma('vm:entry-point') 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'), ], ), ), ); } }