flutter/packages/flutter/test/material/date_range_picker_test.dart
Taha Tesser 755cf0bd3f
Fix Material 3 AppBar.leading action IconButtons (#154512)
Fixes [`AppBar` back button focus/hover circle should not fill up whole height](https://github.com/flutter/flutter/issues/141361)
Fixes [[Material 3] Date Range Picker close button has incorrect shape](https://github.com/flutter/flutter/issues/154393)

This updates the leading condition added in https://github.com/flutter/flutter/pull/110722

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: SingleChildScrollView(
          child: Column(
            children: [
              Column(
                spacing: 10.0,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  AppBar(
                    leading: BackButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    title: const Text('AppBar with BackButton'),
                  ),
                  AppBar(
                    leading: CloseButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    title: const Text('AppBar with CloseButton'),
                  ),
                  AppBar(
                    leading: DrawerButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    title: const Text('AppBar with DrawerButton'),
                  ),
                ],
              ),
              const Divider(),
              Column(
                spacing: 10.0,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  AppBar(
                    leading: BackButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    toolbarHeight: 100.0,
                    title: const Text('AppBar with custom height'),
                  ),
                  AppBar(
                    leading: CloseButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    toolbarHeight: 100.0,
                    title: const Text('AppBar with custom height'),
                  ),
                  AppBar(
                    leading: DrawerButton(
                      style: IconButton.styleFrom(backgroundColor: Colors.red),
                    ),
                    backgroundColor: Theme.of(context).colorScheme.secondaryContainer,
                    toolbarHeight: 100.0,
                    title: const Text('AppBar with custom height'),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

</details>

### Before

<img width="912" alt="Screenshot 2024-09-04 at 12 38 05" src="https://github.com/user-attachments/assets/25a6893c-89c9-4b45-a5bb-8da0eee71cd2">

### After

<img width="912" alt="Screenshot 2024-09-04 at 12 38 28" src="https://github.com/user-attachments/assets/49727183-568c-412e-9fa1-1eefd0cd87a7">
2024-09-06 21:10:35 +00:00

1874 lines
74 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 '../widgets/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;
});
const Size wideWindowSize = Size(1920.0, 1080.0);
const Size narrowWindowSize = Size(1070.0, 1770.0);
Future<void> preparePicker(
WidgetTester tester,
Future<void> Function(Future<DateTimeRange?> date) callback, {
TextDirection textDirection = TextDirection.ltr,
bool useMaterial3 = false,
SelectableDayForRangePredicate? selectableDayPredicate,
}) 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,
selectableDayPredicate: selectableDayPredicate,
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.ancestor(
of: find.byType(IconButton),
matching: find.byType(Center),
));
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 || isSkiaWeb) { // https://github.com/flutter/flutter/issues/99933
expect(saveButtonBottomLeft.dy, helpTextTopLeft.dy);
}
expect(entryButtonBottomLeft.dx, saveButtonBottomLeft.dx - 48.0);
if (!kIsWeb || isSkiaWeb) { // 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 || isSkiaWeb) { // 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.surfaceContainerHigh);
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.surfaceContainerHigh);
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();
});
testWidgets('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();
});
});
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('Long helpText does not cutoff the save button', (WidgetTester tester) async {
helpText = 'long helpText' * 100;
saveText = 'make it so';
await preparePicker(tester, (Future<DateTimeRange?> range) async {
expect(find.text(helpText!), findsOneWidget);
expect(find.text(saveText!), findsOneWidget);
expect(tester.takeException(), null);
});
});
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 select a range even if the range includes non selectable days', (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'));
// The day 13 is not selectable, but the range is still valid.
expect(await range, DateTimeRange(
start: DateTime(2016, DateTime.january, 12),
end: DateTime(2016, DateTime.january, 14),
));
}, selectableDayPredicate: (DateTime day, _, __) => day.day != 13);
});
testWidgets('Cannot select a day inside bounds but not selectable', (WidgetTester tester) async {
initialDateRange = DateTimeRange(
start: DateTime(2017, DateTime.january, 13),
end: DateTime(2017, DateTime.january, 14),
);
firstDate = DateTime(2017, DateTime.january, 12);
lastDate = DateTime(2017, DateTime.january, 16);
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Non-selectable date. Should be ignored.
await tester.tap(find.text('15'));
await tester.tap(find.text('SAVE'));
// We should still be on the initial date.
expect(await range, initialDateRange);
}, selectableDayPredicate: (DateTime day, _, __) => day.day != 15);
});
testWidgets('Selectable date becoming non selectable when selected start day', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
await tester.tap(find.text('12').first);
await tester.pumpAndSettle();
await tester.tap(find.text('11').first);
await tester.pumpAndSettle();
await tester.tap(find.text('14').first);
await tester.pumpAndSettle();
await tester.tap(find.text('SAVE'));
expect(await range, DateTimeRange(
start: DateTime(2016, DateTime.january, 12),
end: DateTime(2016, DateTime.january, 14),
));
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
if (selectedEnd == null && selectedStart != null) {
return day == selectedStart || day.isAfter(selectedStart);
}
return true;
});
});
testWidgets('selectableDayPredicate should be called with the selected start and end dates', (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 {
}, selectableDayPredicate: (DateTime day, DateTime? selectedStartDate, DateTime? selectedEndDate) {
expect(selectedStartDate, DateTime(2017, DateTime.january, 13));
expect(selectedEndDate, DateTime(2017, DateTime.january, 15));
return true;
});
});
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('Non-selectable start date', (WidgetTester tester) async {
// Even if start and end dates are selected, the start date is not selectable
// ending up to no date selected at all in calendar mode.
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/2016');
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
expect(find.text('Start Date'), findsOneWidget);
expect(find.text('End Date'), findsOneWidget);
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
return day != DateTime(2016, DateTime.december, 24);
});
});
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('Non-selectable end date', (WidgetTester tester) async {
// The end date is not selectable, so only the start date should be 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/2016');
await tester.tap(find.byIcon(Icons.calendar_today));
await tester.pumpAndSettle();
expect(find.text('Dec 24'), findsOneWidget);
expect(find.text('End Date'), findsOneWidget);
}, selectableDayPredicate: (DateTime day, DateTime? selectedStart, DateTime? selectedEnd) {
return day != DateTime(2016, DateTime.december, 25);
});
});
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.surfaceContainerHigh);
expect(dialogMaterial.shadowColor, Colors.transparent);
expect(dialogMaterial.surfaceTintColor, Colors.transparent);
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);
});
// This is a regression test for https://github.com/flutter/flutter/issues/131989.
testWidgets('Dialog contents do not overflow when resized from landscape to portrait',
(WidgetTester tester) async {
addTearDown(tester.view.reset);
// Initial window size is wide for landscape mode.
tester.view.physicalSize = wideWindowSize;
tester.view.devicePixelRatio = 1.0;
await preparePicker(tester, (Future<DateTimeRange?> range) async {
// Change window size to narrow for portrait mode.
tester.view.physicalSize = narrowWindowSize;
await tester.pump();
expect(tester.takeException(), null);
});
});
});
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,
hasFocusAction: 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();
});
// This is a regression test for https://github.com/flutter/flutter/issues/154393.
testWidgets('DateRangePicker close button shape should be square', (WidgetTester tester) async {
await preparePicker(tester, (Future<DateTimeRange?> range) async {
final ThemeData theme = ThemeData();
final Finder buttonFinder = find.widgetWithIcon(IconButton, Icons.close);
expect(tester.getSize(buttonFinder), const Size(48.0, 48.0));
// Test the close button overlay size is square.
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
expect(
buttonFinder,
paints
..rect(
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 40.0),
color: theme.colorScheme.onSurfaceVariant.withOpacity(0.08),
),
);
}, useMaterial3: true);
});
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, theme.colorScheme.primary);
});
});
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 dispose() {
_startDate.dispose();
_endDate.dispose();
_restorableDateRangePickerRouteFuture.dispose();
super.dispose();
}
@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'),
],
),
),
);
}
}