1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/material.dart';
7import 'package:flutter/semantics.dart';
8import 'package:flutter/services.dart';
9import 'package:flutter_test/flutter_test.dart';
10
11import '../widgets/clipboard_utils.dart';
12
13class TestMaterialLocalizations extends DefaultMaterialLocalizations {
14 @override
15 String formatCompactDate(DateTime date) {
16 return '${date.month}/${date.day}/${date.year}';
17 }
18}
19
20class TestMaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
21 @override
22 bool isSupported(Locale locale) => true;
23
24 @override
25 Future<MaterialLocalizations> load(Locale locale) {
26 return SynchronousFuture<MaterialLocalizations>(TestMaterialLocalizations());
27 }
28
29 @override
30 bool shouldReload(TestMaterialLocalizationsDelegate old) => false;
31}
32
33void main() {
34 TestWidgetsFlutterBinding.ensureInitialized();
35 final MockClipboard mockClipboard = MockClipboard();
36
37 Widget inputDatePickerField({
38 Key? key,
39 DateTime? initialDate,
40 DateTime? firstDate,
41 DateTime? lastDate,
42 ValueChanged<DateTime>? onDateSubmitted,
43 ValueChanged<DateTime>? onDateSaved,
44 SelectableDayPredicate? selectableDayPredicate,
45 String? errorFormatText,
46 String? errorInvalidText,
47 String? fieldHintText,
48 String? fieldLabelText,
49 bool autofocus = false,
50 Key? formKey,
51 ThemeData? theme,
52 Iterable<LocalizationsDelegate<dynamic>>? localizationsDelegates,
53 bool acceptEmptyDate = false,
54 FocusNode? focusNode,
55 }) {
56 return MaterialApp(
57 theme: theme ?? ThemeData.from(colorScheme: const ColorScheme.light()),
58 localizationsDelegates: localizationsDelegates,
59 home: Material(
60 child: Form(
61 key: formKey,
62 child: InputDatePickerFormField(
63 key: key,
64 initialDate: initialDate ?? DateTime(2016, DateTime.january, 15),
65 firstDate: firstDate ?? DateTime(2001),
66 lastDate: lastDate ?? DateTime(2031, DateTime.december, 31),
67 onDateSubmitted: onDateSubmitted,
68 onDateSaved: onDateSaved,
69 selectableDayPredicate: selectableDayPredicate,
70 errorFormatText: errorFormatText,
71 errorInvalidText: errorInvalidText,
72 fieldHintText: fieldHintText,
73 fieldLabelText: fieldLabelText,
74 autofocus: autofocus,
75 acceptEmptyDate: acceptEmptyDate,
76 focusNode: focusNode,
77 ),
78 ),
79 ),
80 );
81 }
82
83 TextField textField(WidgetTester tester) {
84 return tester.widget<TextField>(find.byType(TextField));
85 }
86
87 TextEditingController textFieldController(WidgetTester tester) {
88 return textField(tester).controller!;
89 }
90
91 double textOpacity(WidgetTester tester, String textValue) {
92 final FadeTransition opacityWidget = tester.widget<FadeTransition>(
93 find.ancestor(of: find.text(textValue), matching: find.byType(FadeTransition)).first,
94 );
95 return opacityWidget.opacity.value;
96 }
97
98 group('InputDatePickerFormField', () {
99 testWidgets('Initial date is the default', (WidgetTester tester) async {
100 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
101 final DateTime initialDate = DateTime(2016, DateTime.february, 21);
102 DateTime? inputDate;
103 await tester.pumpWidget(
104 inputDatePickerField(
105 initialDate: initialDate,
106 onDateSaved: (DateTime date) => inputDate = date,
107 formKey: formKey,
108 ),
109 );
110 expect(textFieldController(tester).value.text, equals('02/21/2016'));
111 formKey.currentState!.save();
112 expect(inputDate, equals(initialDate));
113 });
114
115 testWidgets('Changing initial date is reflected in text value', (WidgetTester tester) async {
116 final DateTime initialDate = DateTime(2016, DateTime.february, 21);
117 final DateTime updatedInitialDate = DateTime(2016, DateTime.february, 23);
118 await tester.pumpWidget(inputDatePickerField(initialDate: initialDate));
119 expect(textFieldController(tester).value.text, equals('02/21/2016'));
120
121 await tester.pumpWidget(inputDatePickerField(initialDate: updatedInitialDate));
122 await tester.pumpAndSettle();
123 expect(textFieldController(tester).value.text, equals('02/23/2016'));
124 });
125
126 testWidgets('Valid date entry', (WidgetTester tester) async {
127 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
128 DateTime? inputDate;
129 await tester.pumpWidget(
130 inputDatePickerField(onDateSaved: (DateTime date) => inputDate = date, formKey: formKey),
131 );
132
133 textFieldController(tester).text = '02/21/2016';
134 formKey.currentState!.save();
135 expect(inputDate, equals(DateTime(2016, DateTime.february, 21)));
136 });
137
138 testWidgets('Invalid text entry shows errorFormat text', (WidgetTester tester) async {
139 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
140 DateTime? inputDate;
141 await tester.pumpWidget(
142 inputDatePickerField(onDateSaved: (DateTime date) => inputDate = date, formKey: formKey),
143 );
144 // Default errorFormat text
145 expect(find.text('Invalid format.'), findsNothing);
146 await tester.enterText(find.byType(TextField), 'foobar');
147 expect(formKey.currentState!.validate(), isFalse);
148 await tester.pumpAndSettle();
149 expect(inputDate, isNull);
150 expect(find.text('Invalid format.'), findsOneWidget);
151
152 // Change to a custom errorFormat text
153 await tester.pumpWidget(
154 inputDatePickerField(
155 onDateSaved: (DateTime date) => inputDate = date,
156 errorFormatText: 'That is not a date.',
157 formKey: formKey,
158 ),
159 );
160 expect(formKey.currentState!.validate(), isFalse);
161 await tester.pumpAndSettle();
162 expect(find.text('Invalid format.'), findsNothing);
163 expect(find.text('That is not a date.'), findsOneWidget);
164 });
165
166 testWidgets(
167 'Valid text entry, but date outside first or last date shows bounds shows errorInvalid text',
168 (WidgetTester tester) async {
169 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
170 DateTime? inputDate;
171 await tester.pumpWidget(
172 inputDatePickerField(
173 firstDate: DateTime(1966, DateTime.february, 21),
174 lastDate: DateTime(2040, DateTime.february, 23),
175 onDateSaved: (DateTime date) => inputDate = date,
176 formKey: formKey,
177 ),
178 );
179 // Default errorInvalid text
180 expect(find.text('Out of range.'), findsNothing);
181 // Before first date
182 await tester.enterText(find.byType(TextField), '02/21/1950');
183 expect(formKey.currentState!.validate(), isFalse);
184 await tester.pumpAndSettle();
185 expect(inputDate, isNull);
186 expect(find.text('Out of range.'), findsOneWidget);
187 // After last date
188 await tester.enterText(find.byType(TextField), '02/23/2050');
189 expect(formKey.currentState!.validate(), isFalse);
190 await tester.pumpAndSettle();
191 expect(inputDate, isNull);
192 expect(find.text('Out of range.'), findsOneWidget);
193
194 await tester.pumpWidget(
195 inputDatePickerField(
196 onDateSaved: (DateTime date) => inputDate = date,
197 errorInvalidText: 'Not in given range.',
198 formKey: formKey,
199 ),
200 );
201 expect(formKey.currentState!.validate(), isFalse);
202 await tester.pumpAndSettle();
203 expect(find.text('Out of range.'), findsNothing);
204 expect(find.text('Not in given range.'), findsOneWidget);
205 },
206 );
207
208 testWidgets(
209 'selectableDatePredicate will be used to show errorInvalid if date is not selectable',
210 (WidgetTester tester) async {
211 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
212 DateTime? inputDate;
213 await tester.pumpWidget(
214 inputDatePickerField(
215 initialDate: DateTime(2016, DateTime.january, 16),
216 onDateSaved: (DateTime date) => inputDate = date,
217 selectableDayPredicate: (DateTime date) => date.day.isEven,
218 formKey: formKey,
219 ),
220 );
221 // Default errorInvalid text
222 expect(find.text('Out of range.'), findsNothing);
223 // Odd day shouldn't be valid
224 await tester.enterText(find.byType(TextField), '02/21/1966');
225 expect(formKey.currentState!.validate(), isFalse);
226 await tester.pumpAndSettle();
227 expect(inputDate, isNull);
228 expect(find.text('Out of range.'), findsOneWidget);
229 // Even day is valid
230 await tester.enterText(find.byType(TextField), '02/24/2030');
231 expect(formKey.currentState!.validate(), isTrue);
232 formKey.currentState!.save();
233 await tester.pumpAndSettle();
234 expect(inputDate, equals(DateTime(2030, DateTime.february, 24)));
235 expect(find.text('Out of range.'), findsNothing);
236 },
237 );
238
239 testWidgets('Empty field shows hint text when focused', (WidgetTester tester) async {
240 await tester.pumpWidget(inputDatePickerField());
241 // Focus on it
242 await tester.tap(find.byType(TextField));
243 await tester.pumpAndSettle();
244
245 // Hint text should be invisible
246 expect(textOpacity(tester, 'mm/dd/yyyy'), equals(0.0));
247 textFieldController(tester).clear();
248 await tester.pumpAndSettle();
249 // Hint text should be visible
250 expect(textOpacity(tester, 'mm/dd/yyyy'), equals(1.0));
251
252 // Change to a different hint text
253 await tester.pumpWidget(inputDatePickerField(fieldHintText: 'Enter some date'));
254 await tester.pumpAndSettle();
255 expect(find.text('mm/dd/yyyy'), findsNothing);
256 expect(textOpacity(tester, 'Enter some date'), equals(1.0));
257 await tester.enterText(find.byType(TextField), 'foobar');
258 await tester.pumpAndSettle();
259 expect(textOpacity(tester, 'Enter some date'), equals(0.0));
260 });
261
262 testWidgets('Label text', (WidgetTester tester) async {
263 await tester.pumpWidget(inputDatePickerField());
264 // Default label
265 expect(find.text('Enter Date'), findsOneWidget);
266
267 await tester.pumpWidget(inputDatePickerField(fieldLabelText: 'Give me a date!'));
268 expect(find.text('Enter Date'), findsNothing);
269 expect(find.text('Give me a date!'), findsOneWidget);
270 });
271
272 testWidgets('Semantics', (WidgetTester tester) async {
273 final SemanticsHandle semantics = tester.ensureSemantics();
274
275 // Fill the clipboard so that the Paste option is available in the text
276 // selection menu.
277 tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
278 SystemChannels.platform,
279 mockClipboard.handleMethodCall,
280 );
281 await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
282 addTearDown(
283 () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
284 SystemChannels.platform,
285 null,
286 ),
287 );
288
289 await tester.pumpWidget(inputDatePickerField(autofocus: true));
290 await tester.pumpAndSettle();
291
292 expect(
293 tester.getSemantics(find.byType(EditableText)),
294 matchesSemantics(
295 label: 'Enter Date',
296 isTextField: true,
297 hasEnabledState: true,
298 isEnabled: true,
299 isFocused: true,
300 value: '01/15/2016',
301 hasTapAction: true,
302 hasFocusAction: true,
303 hasSetTextAction: true,
304 hasSetSelectionAction: true,
305 hasCopyAction: true,
306 hasCutAction: true,
307 hasPasteAction: true,
308 hasMoveCursorBackwardByCharacterAction: true,
309 hasMoveCursorBackwardByWordAction: true,
310 validationResult: SemanticsValidationResult.valid,
311 ),
312 );
313 semantics.dispose();
314 });
315
316 testWidgets('InputDecorationTheme is honored', (WidgetTester tester) async {
317 const InputBorder border = InputBorder.none;
318 await tester.pumpWidget(
319 inputDatePickerField(
320 theme: ThemeData.from(
321 colorScheme: const ColorScheme.light(),
322 ).copyWith(inputDecorationTheme: const InputDecorationTheme(border: border)),
323 ),
324 );
325 await tester.pumpAndSettle();
326
327 // Get the border and container color from the painter of the _BorderContainer
328 // (this was cribbed from input_decorator_test.dart).
329 final CustomPaint customPaint = tester.widget(
330 find.descendant(
331 of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
332 matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
333 ),
334 );
335 final dynamic /*_InputBorderPainter*/ inputBorderPainter = customPaint.foregroundPainter;
336 // ignore: avoid_dynamic_calls
337 final dynamic /*_InputBorderTween*/ inputBorderTween = inputBorderPainter.border;
338 // ignore: avoid_dynamic_calls
339 final Animation<double> animation = inputBorderPainter.borderAnimation as Animation<double>;
340 // ignore: avoid_dynamic_calls
341 final InputBorder actualBorder = inputBorderTween.evaluate(animation) as InputBorder;
342 // ignore: avoid_dynamic_calls
343 final Color containerColor = inputBorderPainter.blendedColor as Color;
344
345 // Border should match
346 expect(actualBorder, equals(border));
347
348 // It shouldn't be filled, so the color should be transparent
349 expect(containerColor, equals(Colors.transparent));
350 });
351
352 testWidgets('Date text localization', (WidgetTester tester) async {
353 final Iterable<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[
354 TestMaterialLocalizationsDelegate(),
355 DefaultWidgetsLocalizations.delegate,
356 ];
357 await tester.pumpWidget(inputDatePickerField(localizationsDelegates: delegates));
358 await tester.enterText(find.byType(TextField), '01/01/2022');
359 await tester.pumpAndSettle();
360
361 // Verify that the widget can be updated to a new value after the
362 // entered text was transformed by the localization formatter.
363 await tester.pumpWidget(
364 inputDatePickerField(initialDate: DateTime(2017), localizationsDelegates: delegates),
365 );
366 });
367
368 testWidgets(
369 'when an empty date is entered and acceptEmptyDate is true, then errorFormatText is not shown',
370 (WidgetTester tester) async {
371 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
372 const String errorFormatText = 'That is not a date.';
373 await tester.pumpWidget(
374 inputDatePickerField(
375 errorFormatText: errorFormatText,
376 formKey: formKey,
377 acceptEmptyDate: true,
378 ),
379 );
380 await tester.enterText(find.byType(TextField), '');
381 await tester.pumpAndSettle();
382 formKey.currentState!.validate();
383 await tester.pumpAndSettle();
384 expect(find.text(errorFormatText), findsNothing);
385 },
386 );
387
388 testWidgets(
389 'when an empty date is entered and acceptEmptyDate is false, then errorFormatText is shown',
390 (WidgetTester tester) async {
391 final GlobalKey<FormState> formKey = GlobalKey<FormState>();
392 const String errorFormatText = 'That is not a date.';
393 await tester.pumpWidget(
394 inputDatePickerField(errorFormatText: errorFormatText, formKey: formKey),
395 );
396 await tester.enterText(find.byType(TextField), '');
397 await tester.pumpAndSettle();
398 formKey.currentState!.validate();
399 await tester.pumpAndSettle();
400 expect(find.text(errorFormatText), findsOneWidget);
401 },
402 );
403 });
404
405 testWidgets('FocusNode can request focus', (WidgetTester tester) async {
406 final FocusNode focusNode = FocusNode();
407 addTearDown(focusNode.dispose);
408 await tester.pumpWidget(inputDatePickerField(focusNode: focusNode));
409 expect((tester.widget(find.byType(TextField)) as TextField).focusNode, focusNode);
410 expect(focusNode.hasFocus, isFalse);
411 focusNode.requestFocus();
412 await tester.pumpAndSettle();
413 expect(focusNode.hasFocus, isTrue);
414 focusNode.unfocus();
415 await tester.pumpAndSettle();
416 expect(focusNode.hasFocus, isFalse);
417 });
418
419 group('Calendar Delegate', () {
420 testWidgets('Defaults to Gregorian calendar system', (WidgetTester tester) async {
421 await tester.pumpWidget(
422 MaterialApp(
423 theme: ThemeData(useMaterial3: true),
424 home: Material(
425 child: InputDatePickerFormField(
426 initialDate: DateTime(2025, DateTime.february, 26),
427 firstDate: DateTime(2025, DateTime.february),
428 lastDate: DateTime(2026, DateTime.may),
429 ),
430 ),
431 ),
432 );
433
434 final InputDatePickerFormField inputDatePickerField = tester.widget(
435 find.byType(InputDatePickerFormField),
436 );
437 expect(inputDatePickerField.calendarDelegate, isA<GregorianCalendarDelegate>());
438 });
439
440 testWidgets('Using custom calendar delegate implementation', (WidgetTester tester) async {
441 await tester.pumpWidget(
442 MaterialApp(
443 theme: ThemeData(useMaterial3: true),
444 home: Material(
445 child: InputDatePickerFormField(
446 initialDate: DateTime(2025, DateTime.february, 26),
447 firstDate: DateTime(2025, DateTime.february),
448 lastDate: DateTime(2026, DateTime.may),
449 calendarDelegate: const TestCalendarDelegate(),
450 ),
451 ),
452 ),
453 );
454
455 final InputDatePickerFormField inputDatePickerField = tester.widget(
456 find.byType(InputDatePickerFormField),
457 );
458 expect(inputDatePickerField.calendarDelegate, isA<TestCalendarDelegate>());
459 });
460
461 testWidgets('Displays calendar based on the calendar delegate', (WidgetTester tester) async {
462 DateTime? selectedDate;
463
464 await tester.pumpWidget(
465 MaterialApp(
466 theme: ThemeData(useMaterial3: true),
467 home: Material(
468 child: InputDatePickerFormField(
469 initialDate: DateTime(2025, DateTime.february, 26),
470 firstDate: DateTime(2025, DateTime.february),
471 lastDate: DateTime(2026, DateTime.may),
472 onDateSubmitted: (DateTime value) {
473 selectedDate = value;
474 },
475 calendarDelegate: const TestCalendarDelegate(),
476 ),
477 ),
478 ),
479 );
480
481 final Finder dateInput1 = find.descendant(
482 of: find.byType(TextField),
483 matching: find.text('2025..2..26'),
484 );
485 expect(dateInput1, findsOneWidget);
486
487 await tester.tap(dateInput1);
488 await tester.pumpAndSettle();
489
490 await tester.enterText(dateInput1, '2025..3..10');
491 await tester.testTextInput.receiveAction(TextInputAction.done);
492 await tester.pumpAndSettle();
493
494 expect(selectedDate, DateTime(2025, DateTime.march, 10));
495
496 final Finder dateInput2 = find.descendant(
497 of: find.byType(TextField),
498 matching: find.text('2025..3..10'),
499 );
500 expect(dateInput2, findsOneWidget);
501
502 await tester.tap(dateInput2);
503 await tester.pumpAndSettle();
504
505 await tester.enterText(dateInput2, '2025..4..21');
506 await tester.testTextInput.receiveAction(TextInputAction.done);
507 await tester.pumpAndSettle();
508
509 expect(selectedDate, DateTime(2025, DateTime.april, 21));
510 });
511 });
512}
513
514class TestCalendarDelegate extends GregorianCalendarDelegate {
515 const TestCalendarDelegate();
516
517 @override
518 String formatCompactDate(DateTime date, MaterialLocalizations localizations) {
519 return '${date.year}..${date.month}..${date.day}';
520 }
521
522 @override
523 DateTime? parseCompactDate(String? inputString, MaterialLocalizations localizations) {
524 final List<String> parts = inputString!.split('..');
525 if (parts.length != 3) {
526 return null;
527 }
528 final int year = int.tryParse(parts[0]) ?? 0;
529 final int month = int.tryParse(parts[1]) ?? 0;
530 final int day = int.tryParse(parts[2]) ?? 0;
531 return DateTime(year, month, day);
532 }
533
534 @override
535 String dateHelpText(MaterialLocalizations localizations) {
536 return 'yyyy..mm..dd';
537 }
538}
539

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com