time_picker_test.dart 43.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hans Muller's avatar
Hans Muller committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
@TestOn('!chrome')
Hans Muller's avatar
Hans Muller committed
6
import 'package:flutter/material.dart';
Yegor's avatar
Yegor committed
7
import 'package:flutter/rendering.dart';
Hans Muller's avatar
Hans Muller committed
8 9
import 'package:flutter_test/flutter_test.dart';

10
import '../widgets/semantics_tester.dart';
11 12
import 'feedback_tester.dart';

13 14
final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl');
15
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == 'TimePickerDialog');
16

17
class _TimePickerLauncher extends StatefulWidget {
18
  const _TimePickerLauncher({
19 20
    Key? key,
    required this.onChanged,
21
    this.entryMode = TimePickerEntryMode.dial,
22
    this.restorationId,
23
  }) : super(key: key);
24

25
  final ValueChanged<TimeOfDay?> onChanged;
26
  final TimePickerEntryMode entryMode;
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
  final String? restorationId;

  @override
  _TimePickerLauncherState createState() => _TimePickerLauncherState();
}

class _TimePickerLauncherState extends State<_TimePickerLauncher> with RestorationMixin {
  @override
  String? get restorationId => widget.restorationId;

  late final RestorableRouteFuture<TimeOfDay?> _restorableTimePickerRouteFuture = RestorableRouteFuture<TimeOfDay?>(
    onComplete: _selectTime,
    onPresent: (NavigatorState navigator, Object? arguments) {
      return navigator.restorablePush(
        _timePickerRoute,
        arguments: <String, int>{
          'entryMode': widget.entryMode.index,
        },
      );
    },
  );

  static Route<TimeOfDay> _timePickerRoute(
    BuildContext context,
    Object? arguments,
  ) {
    final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>;
    final TimePickerEntryMode entryMode = TimePickerEntryMode.values[args['entryMode'] as int];
    return DialogRoute<TimeOfDay>(
      context: context,
      builder: (BuildContext context) {
        return TimePickerDialog(
          restorationId: 'time_picker_dialog',
          initialTime: const TimeOfDay(hour: 7, minute: 0),
          initialEntryMode: entryMode,
        );
      },
    );
  }

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_restorableTimePickerRouteFuture, 'time_picker_route_future');
  }

  void _selectTime(TimeOfDay? newSelectedTime) {
    widget.onChanged(newSelectedTime);
  }
75 76 77

  @override
  Widget build(BuildContext context) {
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    return Material(
      child: Center(
        child: Builder(
          builder: (BuildContext context) {
            return ElevatedButton(
              child: const Text('X'),
              onPressed: () async {
                if (widget.restorationId == null) {
                  widget.onChanged(await showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                    initialEntryMode: widget.entryMode,
                  ));
                } else {
                  _restorableTimePickerRouteFuture.present();
                }
              },
            );
          },
97 98
        ),
      ),
99
    );
100 101
  }
}
Hans Muller's avatar
Hans Muller committed
102

103
Future<Offset?> startPicker(
104
    WidgetTester tester,
105
    ValueChanged<TimeOfDay?> onChanged, {
106
      TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
107
      String? restorationId,
108
    }) async {
109 110 111 112 113 114 115 116 117
  await tester.pumpWidget(MaterialApp(
    restorationScopeId: 'app',
    locale: const Locale('en', 'US'),
    home: _TimePickerLauncher(
      onChanged: onChanged,
      entryMode: entryMode,
      restorationId: restorationId,
    ),
  ));
118
  await tester.tap(find.text('X'));
119
  await tester.pumpAndSettle(const Duration(seconds: 1));
120
  return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
121
}
Hans Muller's avatar
Hans Muller committed
122

123
Future<void> finishPicker(WidgetTester tester) async {
124
  final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(ElevatedButton)));
Yegor's avatar
Yegor committed
125
  await tester.tap(find.text(materialLocalizations.okButtonLabel));
126
  await tester.pumpAndSettle(const Duration(seconds: 1));
127
}
Hans Muller's avatar
Hans Muller committed
128

129
void main() {
130
  group('Time picker - Dial', () {
131 132
    _tests();
  });
133 134 135 136

  group('Time picker - Input', () {
    _testsInput();
  });
137 138 139
}

void _tests() {
140
  testWidgets('tap-select an hour', (WidgetTester tester) async {
141
    TimeOfDay? result;
142

143
    Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
144
    await tester.tapAt(Offset(center.dx, center.dy - 50.0)); // 12:00 AM
145 146 147
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));

148
    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
149
    await tester.tapAt(Offset(center.dx + 50.0, center.dy));
150 151 152
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 3, minute: 0)));

153
    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
154
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
155 156 157
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 0)));

158
    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
159 160
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
    await tester.tapAt(Offset(center.dx - 50, center.dy));
161 162
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 0)));
Ian Hickson's avatar
Ian Hickson committed
163 164
  });

165
  testWidgets('drag-select an hour', (WidgetTester tester) async {
166
    late TimeOfDay result;
167

168
    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
169 170 171 172
    final Offset hour0 = Offset(center.dx, center.dy - 50.0); // 12:00 AM
    final Offset hour3 = Offset(center.dx + 50.0, center.dy);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0);
    final Offset hour9 = Offset(center.dx - 50.0, center.dy);
173

174 175 176 177 178
    TestGesture gesture;

    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour0 - hour3);
    await gesture.up();
179 180
    await finishPicker(tester);
    expect(result.hour, 0);
181

182
    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
183 184 185
    gesture = await tester.startGesture(hour0);
    await gesture.moveBy(hour3 - hour0);
    await gesture.up();
186 187
    await finishPicker(tester);
    expect(result.hour, 3);
188

189
    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
190 191 192
    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour6 - hour3);
    await gesture.up();
193 194
    await finishPicker(tester);
    expect(result.hour, equals(6));
195

196
    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
197 198 199
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
200 201
    await finishPicker(tester);
    expect(result.hour, equals(9));
Hans Muller's avatar
Hans Muller committed
202
  });
203

204
  testWidgets('tap-select switches from hour to minute', (WidgetTester tester) async {
205
    late TimeOfDay result;
206

207
    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
208 209 210 211 212 213 214 215 216 217 218
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min45);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('drag-select switches from hour to minute', (WidgetTester tester) async {
219
    late TimeOfDay result;
220

221
    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    final Offset hour3 = Offset(center.dx + 50.0, center.dy);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0);
    final Offset hour9 = Offset(center.dx - 50.0, center.dy);

    TestGesture gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
    await tester.pump(const Duration(milliseconds: 50));
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour3 - hour6);
    await gesture.up();
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
  });

237
  testWidgets('tap-select rounds down to nearest 5 minute increment', (WidgetTester tester) async {
238
    late TimeOfDay result;
239

240
    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
241 242 243 244 245 246 247 248 249 250 251
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min46 = Offset(center.dx - 50.0, center.dy - 5); // 46 mins

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min46);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('tap-select rounds up to nearest 5 minute increment', (WidgetTester tester) async {
252
    late TimeOfDay result;
253

254
    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
255 256 257 258 259 260 261 262 263 264
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min48 = Offset(center.dx - 50.0, center.dy - 15); // 48 mins

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min48);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 50)));
  });

265
  group('haptic feedback', () {
266 267
    const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
    const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
268
    late FeedbackTester feedback;
269

270
    setUp(() {
271
      feedback = FeedbackTester();
272 273
    });

274
    tearDown(() {
275
      feedback.dispose();
276 277 278
    });

    testWidgets('tap-select vibrates once', (WidgetTester tester) async {
279
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
280
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
281
      await finishPicker(tester);
282
      expect(feedback.hapticCount, 1);
283 284 285
    });

    testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async {
286
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
287
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
288
      await tester.pump(kFastFeedbackInterval);
289
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
290
      await finishPicker(tester);
291
      expect(feedback.hapticCount, 1);
292 293 294
    });

    testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async {
295
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
296
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
297
      await tester.pump(kSlowFeedbackInterval);
298
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
299
      await tester.pump(kSlowFeedbackInterval);
300
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
301
      await finishPicker(tester);
302
      expect(feedback.hapticCount, 3);
303 304 305
    });

    testWidgets('drag-select vibrates once', (WidgetTester tester) async {
306
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
307 308
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
309

310
      final TestGesture gesture = await tester.startGesture(hour3);
311 312 313
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
314
      expect(feedback.hapticCount, 1);
315 316 317
    });

    testWidgets('quick drag-select vibrates once', (WidgetTester tester) async {
318
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
319 320
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
321

322
      final TestGesture gesture = await tester.startGesture(hour3);
323
      await gesture.moveBy(hour0 - hour3);
324
      await tester.pump(kFastFeedbackInterval);
325
      await gesture.moveBy(hour3 - hour0);
326
      await tester.pump(kFastFeedbackInterval);
327 328 329
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
330
      expect(feedback.hapticCount, 1);
331 332 333
    });

    testWidgets('slow drag-select vibrates once', (WidgetTester tester) async {
334
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
335 336
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
337

338
      final TestGesture gesture = await tester.startGesture(hour3);
339
      await gesture.moveBy(hour0 - hour3);
340
      await tester.pump(kSlowFeedbackInterval);
341
      await gesture.moveBy(hour3 - hour0);
342
      await tester.pump(kSlowFeedbackInterval);
343 344 345
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
346
      expect(feedback.hapticCount, 3);
347 348
    });
  });
349

350
  const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
351
  const List<String> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];
352 353 354 355

  testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false);

356
    final CustomPaint dialPaint = tester.widget(findDialPaint);
357
    final dynamic dialPainter = dialPaint.painter;
358
    // ignore: avoid_dynamic_calls
359
    final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
360
    // ignore: avoid_dynamic_calls
361
    expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
362

363
    // ignore: avoid_dynamic_calls
364
    final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
365
    // ignore: avoid_dynamic_calls
366
    expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
367 368 369 370 371
  });

  testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true);

372
    final CustomPaint dialPaint = tester.widget(findDialPaint);
373
    final dynamic dialPainter = dialPaint.painter;
374
    // ignore: avoid_dynamic_calls
375
    final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
376
    // ignore: avoid_dynamic_calls
377 378
    expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);

379
    // ignore: avoid_dynamic_calls
380
    final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
381
    // ignore: avoid_dynamic_calls
382
    expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
383 384 385
  });

  testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
386
    final SemanticsTester semantics = SemanticsTester(tester);
387 388
    await mediaQueryBoilerplate(tester, false);

389 390 391 392 393
    expect(
      semantics,
      includesNodeWith(
        label: 'AM',
        actions: <SemanticsAction>[SemanticsAction.tap],
394 395 396 397 398 399 400
        flags: <SemanticsFlag>[
          SemanticsFlag.isButton,
          SemanticsFlag.isChecked,
          SemanticsFlag.isInMutuallyExclusiveGroup,
          SemanticsFlag.hasCheckedState,
          SemanticsFlag.isFocusable,
        ],
401 402 403 404 405 406 407
      ),
    );
    expect(
      semantics,
      includesNodeWith(
        label: 'PM',
        actions: <SemanticsAction>[SemanticsAction.tap],
408 409 410 411 412 413
        flags: <SemanticsFlag>[
          SemanticsFlag.isButton,
          SemanticsFlag.isInMutuallyExclusiveGroup,
          SemanticsFlag.hasCheckedState,
          SemanticsFlag.isFocusable,
        ],
414 415
      ),
    );
416 417 418 419 420

    semantics.dispose();
  });

  testWidgets('provides semantics information for header and footer', (WidgetTester tester) async {
421
    final SemanticsTester semantics = SemanticsTester(tester);
422 423 424
    await mediaQueryBoilerplate(tester, true);

    expect(semantics, isNot(includesNodeWith(label: ':')));
425 426 427 428 429 430 431 432 433 434
    expect(
      semantics.nodesWith(value: 'Select minutes 00'),
      hasLength(1),
      reason: '00 appears once in the header',
    );
    expect(
      semantics.nodesWith(value: 'Select hours 07'),
      hasLength(1),
      reason: '07 appears once in the header',
    );
435 436 437 438 439 440 441 442 443 444
    expect(semantics, includesNodeWith(label: 'CANCEL'));
    expect(semantics, includesNodeWith(label: 'OK'));

    // In 24-hour mode we don't have AM/PM control.
    expect(semantics, isNot(includesNodeWith(label: 'AM')));
    expect(semantics, isNot(includesNodeWith(label: 'PM')));

    semantics.dispose();
  });

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
  testWidgets('provides semantics information for text fields', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, accessibleNavigation: true);

    expect(
      semantics,
      includesNodeWith(
        label: 'Hour',
        value: '07',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
      ),
    );
    expect(
      semantics,
      includesNodeWith(
        label: 'Minute',
        value: '00',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
      ),
    );

    semantics.dispose();
  });

471
  testWidgets('can increment and decrement hours', (WidgetTester tester) async {
472
    final SemanticsTester semantics = SemanticsTester(tester);
473

474
    Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
475
      final SemanticsNode elevenHours = semantics.nodesWith(
476
        value: 'Select hours $initialValue',
477 478
        ancestor: tester.renderObject(_hourControl).debugSemantics,
      ).single;
479
      tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
      await tester.pumpAndSettle();
      expect(
        find.descendant(of: _hourControl, matching: find.text(finalValue)),
        findsOneWidget,
      );
    }

    // 12-hour format
    await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 0));
    await actAndExpect(
      initialValue: '11',
      action: SemanticsAction.increase,
      finalValue: '12',
    );
    await actAndExpect(
      initialValue: '12',
      action: SemanticsAction.increase,
      finalValue: '1',
    );

    // Ensure we preserve day period as we roll over.
501
    final dynamic pickerState = tester.state(_timePickerDialog);
502
    // ignore: avoid_dynamic_calls
503
    expect(pickerState.selectedTime.value, const TimeOfDay(hour: 1, minute: 0));
504 505 506 507 508 509

    await actAndExpect(
      initialValue: '1',
      action: SemanticsAction.decrease,
      finalValue: '12',
    );
510
    await tester.pumpWidget(Container()); // clear old boilerplate
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533

    // 24-hour format
    await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 23, minute: 0));
    await actAndExpect(
      initialValue: '23',
      action: SemanticsAction.increase,
      finalValue: '00',
    );
    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.increase,
      finalValue: '01',
    );
    await actAndExpect(
      initialValue: '01',
      action: SemanticsAction.decrease,
      finalValue: '00',
    );
    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.decrease,
      finalValue: '23',
    );
534 535

    semantics.dispose();
536 537 538
  });

  testWidgets('can increment and decrement minutes', (WidgetTester tester) async {
539
    final SemanticsTester semantics = SemanticsTester(tester);
540

541
    Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
542
      final SemanticsNode elevenHours = semantics.nodesWith(
543
        value: 'Select minutes $initialValue',
544 545
        ancestor: tester.renderObject(_minuteControl).debugSemantics,
      ).single;
546
      tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
      await tester.pumpAndSettle();
      expect(
        find.descendant(of: _minuteControl, matching: find.text(finalValue)),
        findsOneWidget,
      );
    }

    await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 58));
    await actAndExpect(
      initialValue: '58',
      action: SemanticsAction.increase,
      finalValue: '59',
    );
    await actAndExpect(
      initialValue: '59',
      action: SemanticsAction.increase,
      finalValue: '00',
    );

    // Ensure we preserve hour period as we roll over.
567
    final dynamic pickerState = tester.state(_timePickerDialog);
568
    // ignore: avoid_dynamic_calls
569
    expect(pickerState.selectedTime.value, const TimeOfDay(hour: 11, minute: 0));
570 571 572 573 574 575 576 577 578 579 580

    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.decrease,
      finalValue: '59',
    );
    await actAndExpect(
      initialValue: '59',
      action: SemanticsAction.decrease,
      finalValue: '58',
    );
581 582

    semantics.dispose();
583
  });
584

585
  testWidgets('header touch regions are large enough', (WidgetTester tester) async {
586 587 588
    // Ensure picker is displayed in portrait mode.
    tester.binding.window.physicalSizeTestValue = const Size(400, 800);
    tester.binding.window.devicePixelRatioTestValue = 1;
589 590
    await mediaQueryBoilerplate(tester, false);

591 592 593 594
    final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
    expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
    // Height should be double the minimum size to account for both AM/PM stacked.
    expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));
595 596 597 598 599 600 601 602 603 604 605 606 607 608

    final Size hourSize = tester.getSize(find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    ));
    expect(hourSize.width, greaterThanOrEqualTo(48.0));
    expect(hourSize.height, greaterThanOrEqualTo(48.0));

    final Size minuteSize = tester.getSize(find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    ));
    expect(minuteSize.width, greaterThanOrEqualTo(48.0));
    expect(minuteSize.height, greaterThanOrEqualTo(48.0));
609

610 611
    tester.binding.window.clearPhysicalSizeTestValue();
    tester.binding.window.clearDevicePixelRatioTestValue();
612 613
  });

614 615 616 617 618 619 620
  testWidgets('builder parameter', (WidgetTester tester) async {
    Widget buildFrame(TextDirection textDirection) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
621
                return ElevatedButton(
622 623 624 625 626
                  child: const Text('X'),
                  onPressed: () {
                    showTimePicker(
                      context: context,
                      initialTime: const TimeOfDay(hour: 7, minute: 0),
627
                      builder: (BuildContext context, Widget? child) {
628 629
                        return Directionality(
                          textDirection: textDirection,
630
                          child: child!,
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(TextDirection.ltr));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    await tester.pumpWidget(buildFrame(TextDirection.rtl));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // Verify that the time picker is being laid out RTL.
    // We expect the left edge of the 'OK' button in the RTL
    // layout to match the gap between right edge of the 'OK'
    // button and the right edge of the 800 wide window.
    expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
  });
661 662 663 664 665 666 667 668 669 670 671 672

  testWidgets('uses root navigator by default', (WidgetTester tester) async {
    final PickerObserver rootObserver = PickerObserver();
    final PickerObserver nestedObserver = PickerObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
673
              return ElevatedButton(
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
                onPressed: () {
                  showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
689
    await tester.tap(find.byType(ElevatedButton));
690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705

    expect(rootObserver.pickerCount, 1);
    expect(nestedObserver.pickerCount, 0);
  });

  testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
    final PickerObserver rootObserver = PickerObserver();
    final PickerObserver nestedObserver = PickerObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
706
              return ElevatedButton(
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
                onPressed: () {
                  showTimePicker(
                    context: context,
                    useRootNavigator: false,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
723
    await tester.tap(find.byType(ElevatedButton));
724 725 726 727

    expect(rootObserver.pickerCount, 0);
    expect(nestedObserver.pickerCount, 1);
  });
728

729 730 731 732 733
  testWidgets('optional text parameters are utilized', (WidgetTester tester) async {
    const String cancelText = 'Custom Cancel';
    const String confirmText = 'Custom OK';
    const String helperText = 'Custom Help';
    await tester.pumpWidget(MaterialApp(
734 735 736 737 738 739 740 741 742 743 744 745 746
      home: Material(
        child: Center(
          child: Builder(
            builder: (BuildContext context) {
              return ElevatedButton(
                child: const Text('X'),
                onPressed: () async {
                  await showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                    cancelText: cancelText,
                    confirmText: confirmText,
                    helpText: helperText,
747
                  );
748 749 750
                },
              );
            },
751
          ),
752 753
        ),
      ),
754 755 756
    ));

    // Open the picker.
757
    await tester.tap(find.text('X'));
758
    await tester.pumpAndSettle(const Duration(seconds: 1));
759

760 761 762 763
    expect(find.text(cancelText), findsOneWidget);
    expect(find.text(confirmText), findsOneWidget);
    expect(find.text(helperText), findsOneWidget);
  });
764

765 766 767 768 769 770 771 772 773 774 775 776 777
  testWidgets('OK Cancel button layout', (WidgetTester tester) async {
    Widget buildFrame(TextDirection textDirection) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showTimePicker(
                      context: context,
                      initialTime: const TimeOfDay(hour: 7, minute: 0),
778
                      builder: (BuildContext context, Widget? child) {
779 780
                        return Directionality(
                          textDirection: textDirection,
781
                          child: child!,
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(TextDirection.ltr));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    expect(tester.getBottomRight(find.text('OK')).dx, 638);
    expect(tester.getBottomLeft(find.text('OK')).dx, 610);
    expect(tester.getBottomRight(find.text('CANCEL')).dx, 576);
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();

    await tester.pumpWidget(buildFrame(TextDirection.rtl));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    expect(tester.getBottomLeft(find.text('OK')).dx, 162);
    expect(tester.getBottomRight(find.text('OK')).dx, 190);
    expect(tester.getBottomLeft(find.text('CANCEL')).dx, 224);
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();
  });

  testWidgets('text scale affects certain elements and not others', (WidgetTester tester) async {
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 1.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
    final double amHeight = tester.getSize(find.text('AM')).height;

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    // Verify that the time display is not affected by text scale.
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 2.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    final double amHeight2x = tester.getSize(find.text('AM')).height;
    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(amHeight2x, greaterThanOrEqualTo(amHeight * 2));

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    // Verify that text scale for AM/PM is at most 2x.
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 3.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
  });
853
}
854

855 856 857 858 859
void _testsInput() {
  testWidgets('Initial entry mode is used', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
    expect(find.byType(TextField), findsNWidgets(2));
  });
860

861
  testWidgets('Initial time is the default', (WidgetTester tester) async {
862
    late TimeOfDay result;
863
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
864 865 866
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 7, minute: 0)));
  });
867

868 869 870 871 872
  testWidgets('Help text is used - Input', (WidgetTester tester) async {
    const String helpText = 'help';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, helpText: helpText);
    expect(find.text(helpText), findsOneWidget);
  });
873

874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
  testWidgets('Hour label text is used - Input', (WidgetTester tester) async {
    const String hourLabelText = 'Custom hour label';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, hourLabelText: hourLabelText);
    expect(find.text(hourLabelText), findsOneWidget);
  });


  testWidgets('Minute label text is used - Input', (WidgetTester tester) async {
    const String minuteLabelText = 'Custom minute label';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, minuteLabelText: minuteLabelText);
    expect(find.text(minuteLabelText), findsOneWidget);
  });

  testWidgets('Invalid error text is used - Input', (WidgetTester tester) async {
    const String errorInvalidText = 'Custom validation error';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, errorInvalidText: errorInvalidText);
    // Input invalid time (hour) to force validation error
    await tester.enterText(find.byType(TextField).first, '88');
    final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(TextButton).first));
    // Tap the ok button to trigger the validation error with custom translation
    await tester.tap(find.text(materialLocalizations.okButtonLabel));
    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text(errorInvalidText), findsOneWidget);
  });

899 900 901
  testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
    await tester.tap(find.byIcon(Icons.access_time));
902
    await tester.pumpAndSettle();
903
    expect(find.byType(TextField), findsNothing);
904
  });
905

906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988
  testWidgets('Can double tap hours (when selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder hourFinder = find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Double tap the hour.
    await tester.tap(hourFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(hourFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsWidgets);
  });

  testWidgets('Can not double tap hours (when not selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder hourFinder = find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    );
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Switch to minutes mode.
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    // Double tap the hour.
    await tester.tap(hourFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(hourFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsNothing);
  });

  testWidgets('Can double tap minutes (when selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Switch to minutes mode.
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    // Double tap the minutes.
    await tester.tap(minuteFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsWidgets);
  });

  testWidgets('Can not double tap minutes (when not selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Double tap the minutes.
    await tester.tap(minuteFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsNothing);
  });
989

990
  testWidgets('Entered text returns time', (WidgetTester tester) async {
991
    late TimeOfDay result;
992
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
993 994 995 996 997
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.enterText(find.byType(TextField).last, '12');
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
  });
998

999
  testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
1000
    late TimeOfDay result;
1001
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
1002 1003 1004 1005 1006 1007
    await tester.enterText(find.byType(TextField).first, '8');
    await tester.enterText(find.byType(TextField).last, '15');
    await tester.tap(find.byIcon(Icons.access_time));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
  });
1008

1009
  testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
1010
    TimeOfDay? result;
1011
    await startPicker(tester, (TimeOfDay? time) { result = time; }, entryMode: TimePickerEntryMode.input);
1012

1013 1014 1015 1016 1017
    // Invalid hour.
    await tester.enterText(find.byType(TextField).first, '88');
    await tester.enterText(find.byType(TextField).last, '15');
    await finishPicker(tester);
    expect(result, null);
1018

1019 1020
    // Invalid minute.
    await tester.enterText(find.byType(TextField).first, '8');
1021
    await tester.enterText(find.byType(TextField).last, '95');
1022 1023
    await finishPicker(tester);
    expect(result, null);
1024

1025 1026 1027 1028 1029
    await tester.enterText(find.byType(TextField).first, '8');
    await tester.enterText(find.byType(TextField).last, '15');
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
  });
1030 1031 1032

  // Fixes regression that was reverted in https://github.com/flutter/flutter/pull/64094#pullrequestreview-469836378.
  testWidgets('Ensure hour/minute fields are top-aligned with the separator', (WidgetTester tester) async {
1033
    await startPicker(tester, (TimeOfDay? time) { }, entryMode: TimePickerEntryMode.input);
1034 1035 1036 1037 1038 1039
    final double hourFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField')).dy;
    final double minuteFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy;
    final double separatorTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy;
    expect(hourFieldTop, separatorTop);
    expect(minuteFieldTop, separatorTop);
  });
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135

  testWidgets('Time Picker state restoration test - dial mode', (WidgetTester tester) async {
    TimeOfDay? result;
    final Offset center = (await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      restorationId: 'restorable_time_picker',
    ))!;
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await tester.tapAt(min45);
    await tester.pump(const Duration(milliseconds: 50));
    final TestRestorationData restorationData = await tester.getRestorationData();
    await tester.restartAndRestore();
    // Setting to PM adds 12 hours (18:45)
    await tester.tap(find.text('PM'));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 18, minute: 45)));

    // Test restoring from before PM was selected (6:45)
    await tester.restoreFrom(restorationData);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('Time Picker state restoration test - input mode', (WidgetTester tester) async {
    TimeOfDay? result;
    await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      entryMode: TimePickerEntryMode.input,
      restorationId: 'restorable_time_picker',
    );
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    await tester.enterText(find.byType(TextField).last, '12');
    await tester.pump(const Duration(milliseconds: 50));
    final TestRestorationData restorationData = await tester.getRestorationData();
    await tester.restartAndRestore();

    // Setting to PM adds 12 hours (21:12)
    await tester.tap(find.text('PM'));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 21, minute: 12)));

    // Restoring from before PM was set (9:12)
    await tester.restoreFrom(restorationData);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
  });

  testWidgets('Time Picker state restoration test - switching modes', (WidgetTester tester) async {
    TimeOfDay? result;
    final Offset center = (await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      restorationId: 'restorable_time_picker',
    ))!;

    final TestRestorationData restorationData = await tester.getRestorationData();
    // Switch to input mode from dial mode.
    await tester.tap(find.byIcon(Icons.keyboard));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    // Select time using input mode controls.
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.enterText(find.byType(TextField).last, '12');
    await tester.pump(const Duration(milliseconds: 50));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));

    // Restoring from dial mode.
    await tester.restoreFrom(restorationData);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await tester.tapAt(min45);
    await tester.pump(const Duration(milliseconds: 50));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });
Hans Muller's avatar
Hans Muller committed
1136
}
1137

1138 1139 1140 1141 1142
final Finder findDialPaint = find.descendant(
  of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
  matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
);

1143 1144 1145 1146
class PickerObserver extends NavigatorObserver {
  int pickerCount = 0;

  @override
1147
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
1148
    if (route is DialogRoute) {
1149 1150 1151 1152 1153
      pickerCount++;
    }
    super.didPush(route, previousRoute);
  }
}
1154 1155

Future<void> mediaQueryBoilerplate(
1156 1157 1158 1159 1160 1161
  WidgetTester tester,
  bool alwaysUse24HourFormat, {
  TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
  double textScaleFactor = 1.0,
  TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
  String? helpText,
1162 1163 1164
  String? hourLabelText,
  String? minuteLabelText,
  String? errorInvalidText,
1165 1166
  bool accessibleNavigation = false,
}) async {
1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177
  await tester.pumpWidget(
    Localizations(
      locale: const Locale('en', 'US'),
      delegates: const <LocalizationsDelegate<dynamic>>[
        DefaultMaterialLocalizations.delegate,
        DefaultWidgetsLocalizations.delegate,
      ],
      child: MediaQuery(
        data: MediaQueryData(
          alwaysUse24HourFormat: alwaysUse24HourFormat,
          textScaleFactor: textScaleFactor,
1178
          accessibleNavigation: accessibleNavigation,
1179 1180 1181 1182 1183 1184 1185
        ),
        child: Material(
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<void>(builder: (BuildContext context) {
1186
                  return TextButton(
1187 1188 1189 1190 1191 1192
                    onPressed: () {
                      showTimePicker(
                        context: context,
                        initialTime: initialTime,
                        initialEntryMode: entryMode,
                        helpText: helpText,
1193 1194 1195
                        hourLabelText: hourLabelText,
                        minuteLabelText: minuteLabelText,
                        errorInvalidText: errorInvalidText
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210
                      );
                    },
                    child: const Text('X'),
                  );
                });
              },
            ),
          ),
        ),
      ),
    ),
  );
  await tester.tap(find.text('X'));
  await tester.pumpAndSettle();
}