time_picker_test.dart 31.3 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') // entire file needs triage.
6
import 'dart:async';
7
import 'dart:ui' as ui;
8

Hans Muller's avatar
Hans Muller committed
9
import 'package:flutter/material.dart';
Yegor's avatar
Yegor committed
10 11
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
Hans Muller's avatar
Hans Muller committed
12 13
import 'package:flutter_test/flutter_test.dart';

14 15 16
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
17 18
import 'feedback_tester.dart';

19 20 21 22
final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl');
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_TimePickerDialog');

23
class _TimePickerLauncher extends StatelessWidget {
Yegor's avatar
Yegor committed
24
  const _TimePickerLauncher({ Key key, this.onChanged, this.locale }) : super(key: key);
25 26

  final ValueChanged<TimeOfDay> onChanged;
Yegor's avatar
Yegor committed
27
  final Locale locale;
28 29 30

  @override
  Widget build(BuildContext context) {
31
    return MaterialApp(
Yegor's avatar
Yegor committed
32
      locale: locale,
33 34 35
      home: Material(
        child: Center(
          child: Builder(
36
            builder: (BuildContext context) {
37
              return RaisedButton(
38
                child: const Text('X'),
39 40 41
                onPressed: () async {
                  onChanged(await showTimePicker(
                    context: context,
42
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
43
                  ));
44
                },
45 46
              );
            }
47 48 49
          ),
        ),
      ),
50
    );
51 52
  }
}
Hans Muller's avatar
Hans Muller committed
53

54
Future<Offset> startPicker(WidgetTester tester, ValueChanged<TimeOfDay> onChanged) async {
55
  await tester.pumpWidget(_TimePickerLauncher(onChanged: onChanged, locale: const Locale('en', 'US')));
56
  await tester.tap(find.text('X'));
57
  await tester.pumpAndSettle(const Duration(seconds: 1));
58
  return tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial')));
59
}
Hans Muller's avatar
Hans Muller committed
60

61
Future<void> finishPicker(WidgetTester tester) async {
62
  final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(RaisedButton)));
Yegor's avatar
Yegor committed
63
  await tester.tap(find.text(materialLocalizations.okButtonLabel));
64
  await tester.pumpAndSettle(const Duration(seconds: 1));
65
}
Hans Muller's avatar
Hans Muller committed
66

67
void main() {
68 69 70 71 72 73
  group('Time picker', () {
    _tests();
  });
}

void _tests() {
74 75 76
  testWidgets('tap-select an hour', (WidgetTester tester) async {
    TimeOfDay result;

77
    Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
78
    await tester.tapAt(Offset(center.dx, center.dy - 50.0)); // 12:00 AM
79 80 81 82
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));

    center = await startPicker(tester, (TimeOfDay time) { result = time; });
83
    await tester.tapAt(Offset(center.dx + 50.0, center.dy));
84 85 86 87
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 3, minute: 0)));

    center = await startPicker(tester, (TimeOfDay time) { result = time; });
88
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
89 90 91 92
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 0)));

    center = await startPicker(tester, (TimeOfDay time) { result = time; });
93 94
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
    await tester.tapAt(Offset(center.dx - 50, center.dy));
95 96
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 0)));
Ian Hickson's avatar
Ian Hickson committed
97 98
  });

99
  testWidgets('drag-select an hour', (WidgetTester tester) async {
100
    TimeOfDay result;
101

102
    final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
103 104 105 106
    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);
107

108 109 110 111 112
    TestGesture gesture;

    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour0 - hour3);
    await gesture.up();
113 114
    await finishPicker(tester);
    expect(result.hour, 0);
115

116
    expect(await startPicker(tester, (TimeOfDay time) { result = time; }), equals(center));
117 118 119
    gesture = await tester.startGesture(hour0);
    await gesture.moveBy(hour3 - hour0);
    await gesture.up();
120 121
    await finishPicker(tester);
    expect(result.hour, 3);
122

123
    expect(await startPicker(tester, (TimeOfDay time) { result = time; }), equals(center));
124 125 126
    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour6 - hour3);
    await gesture.up();
127 128
    await finishPicker(tester);
    expect(result.hour, equals(6));
129

130
    expect(await startPicker(tester, (TimeOfDay time) { result = time; }), equals(center));
131 132 133
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
134 135
    await finishPicker(tester);
    expect(result.hour, equals(9));
Hans Muller's avatar
Hans Muller committed
136
  });
137

138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
  testWidgets('tap-select switches from hour to minute', (WidgetTester tester) async {
    TimeOfDay result;

    final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
    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 {
    TimeOfDay result;

    final Offset center = await startPicker(tester, (TimeOfDay time) { result = time; });
    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)));
  });

171
  group('haptic feedback', () {
172 173
    const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
    const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
174
    FeedbackTester feedback;
175

176
    setUp(() {
177
      feedback = FeedbackTester();
178 179
    });

180 181
    tearDown(() {
      feedback?.dispose();
182 183 184
    });

    testWidgets('tap-select vibrates once', (WidgetTester tester) async {
185
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
186
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
187
      await finishPicker(tester);
188
      expect(feedback.hapticCount, 1);
189 190 191
    });

    testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async {
192
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
193
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
194
      await tester.pump(kFastFeedbackInterval);
195
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
196
      await finishPicker(tester);
197
      expect(feedback.hapticCount, 1);
198 199 200
    });

    testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async {
201
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
202
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
203
      await tester.pump(kSlowFeedbackInterval);
204
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
205
      await tester.pump(kSlowFeedbackInterval);
206
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
207
      await finishPicker(tester);
208
      expect(feedback.hapticCount, 3);
209 210 211
    });

    testWidgets('drag-select vibrates once', (WidgetTester tester) async {
212
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
213 214
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
215

216
      final TestGesture gesture = await tester.startGesture(hour3);
217 218 219
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
220
      expect(feedback.hapticCount, 1);
221 222 223
    });

    testWidgets('quick drag-select vibrates once', (WidgetTester tester) async {
224
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
225 226
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
227

228
      final TestGesture gesture = await tester.startGesture(hour3);
229
      await gesture.moveBy(hour0 - hour3);
230
      await tester.pump(kFastFeedbackInterval);
231
      await gesture.moveBy(hour3 - hour0);
232
      await tester.pump(kFastFeedbackInterval);
233 234 235
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
236
      expect(feedback.hapticCount, 1);
237 238 239
    });

    testWidgets('slow drag-select vibrates once', (WidgetTester tester) async {
240
      final Offset center = await startPicker(tester, (TimeOfDay time) { });
241 242
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);
243

244
      final TestGesture gesture = await tester.startGesture(hour3);
245
      await gesture.moveBy(hour0 - hour3);
246
      await tester.pump(kSlowFeedbackInterval);
247
      await gesture.moveBy(hour3 - hour0);
248
      await tester.pump(kSlowFeedbackInterval);
249 250 251
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
252
      expect(feedback.hapticCount, 3);
253 254
    });
  });
255

256 257 258
  const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
  const List<String> labels12To11TwoDigit = <String>['12', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11'];
  const List<String> labels00To23 = <String>['00', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23'];
259

260 261 262 263
  Future<void> mediaQueryBoilerplate(
    WidgetTester tester,
    bool alwaysUse24HourFormat, {
    TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
264
    double textScaleFactor = 1.0,
265
  }) async {
266
    await tester.pumpWidget(
267
      Localizations(
268
        locale: const Locale('en', 'US'),
269
        delegates: const <LocalizationsDelegate<dynamic>>[
270 271 272
          DefaultMaterialLocalizations.delegate,
          DefaultWidgetsLocalizations.delegate,
        ],
273
        child: MediaQuery(
274 275 276 277
          data: MediaQueryData(
            alwaysUse24HourFormat: alwaysUse24HourFormat,
            textScaleFactor: textScaleFactor,
          ),
278 279
          child: Material(
            child: Directionality(
280
              textDirection: TextDirection.ltr,
281
              child: Navigator(
282
                onGenerateRoute: (RouteSettings settings) {
283 284
                  return MaterialPageRoute<void>(builder: (BuildContext context) {
                    return FlatButton(
285 286 287 288 289 290 291 292
                      onPressed: () {
                        showTimePicker(context: context, initialTime: initialTime);
                      },
                      child: const Text('X'),
                    );
                  });
                },
              ),
293 294 295 296 297
            ),
          ),
        ),
      ),
    );
298 299 300

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
301 302 303 304 305
  }

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

306
    final CustomPaint dialPaint = tester.widget(findDialPaint);
307
    final dynamic dialPainter = dialPaint.painter;
308 309
    final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
    expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
310 311
    expect(dialPainter.primaryInnerLabels, null);

312 313
    final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
    expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
314 315 316 317 318 319
    expect(dialPainter.secondaryInnerLabels, null);
  });

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

320
    final CustomPaint dialPaint = tester.widget(findDialPaint);
321
    final dynamic dialPainter = dialPaint.painter;
322 323 324 325 326 327 328 329 330
    final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
    expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
    final List<dynamic> primaryInnerLabels = dialPainter.primaryInnerLabels as List<dynamic>;
    expect(primaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);

    final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
    expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To23);
    final List<dynamic> secondaryInnerLabels = dialPainter.secondaryInnerLabels as List<dynamic>;
    expect(secondaryInnerLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11TwoDigit);
331 332 333
  });

  testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
334
    final SemanticsTester semantics = SemanticsTester(tester);
335 336 337 338 339 340 341 342 343
    await mediaQueryBoilerplate(tester, false);

    expect(semantics, includesNodeWith(label: 'AM', actions: <SemanticsAction>[SemanticsAction.tap]));
    expect(semantics, includesNodeWith(label: 'PM', actions: <SemanticsAction>[SemanticsAction.tap]));

    semantics.dispose();
  });

  testWidgets('provides semantics information for header and footer', (WidgetTester tester) async {
344
    final SemanticsTester semantics = SemanticsTester(tester);
345 346 347
    await mediaQueryBoilerplate(tester, true);

    expect(semantics, isNot(includesNodeWith(label: ':')));
348
    expect(semantics.nodesWith(value: '00'), hasLength(2),
349
        reason: '00 appears once in the header, then again in the dial');
350
    expect(semantics.nodesWith(value: '07'), hasLength(2),
351 352 353 354 355 356 357 358 359 360 361 362
        reason: '07 appears once in the header, then again in the dial');
    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();
  });

  testWidgets('provides semantics information for hours', (WidgetTester tester) async {
363
    final SemanticsTester semantics = SemanticsTester(tester);
364 365 366 367
    await mediaQueryBoilerplate(tester, true);

    final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
    final CustomPainter dialPainter = dialPaint.painter;
368
    final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
369

370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
    painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
    painterTester.addLabel('13', 129.0, 11.5, 177.0, 59.5);
    painterTester.addLabel('14', 160.5, 43.0, 208.5, 91.0);
    painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
    painterTester.addLabel('16', 160.5, 129.0, 208.5, 177.0);
    painterTester.addLabel('17', 129.0, 160.5, 177.0, 208.5);
    painterTester.addLabel('18', 86.0, 172.0, 134.0, 220.0);
    painterTester.addLabel('19', 43.0, 160.5, 91.0, 208.5);
    painterTester.addLabel('20', 11.5, 129.0, 59.5, 177.0);
    painterTester.addLabel('21', 0.0, 86.0, 48.0, 134.0);
    painterTester.addLabel('22', 11.5, 43.0, 59.5, 91.0);
    painterTester.addLabel('23', 43.0, 11.5, 91.0, 59.5);
    painterTester.addLabel('12', 86.0, 36.0, 134.0, 84.0);
    painterTester.addLabel('01', 111.0, 42.7, 159.0, 90.7);
    painterTester.addLabel('02', 129.3, 61.0, 177.3, 109.0);
    painterTester.addLabel('03', 136.0, 86.0, 184.0, 134.0);
    painterTester.addLabel('04', 129.3, 111.0, 177.3, 159.0);
    painterTester.addLabel('05', 111.0, 129.3, 159.0, 177.3);
    painterTester.addLabel('06', 86.0, 136.0, 134.0, 184.0);
    painterTester.addLabel('07', 61.0, 129.3, 109.0, 177.3);
    painterTester.addLabel('08', 42.7, 111.0, 90.7, 159.0);
    painterTester.addLabel('09', 36.0, 86.0, 84.0, 134.0);
    painterTester.addLabel('10', 42.7, 61.0, 90.7, 109.0);
    painterTester.addLabel('11', 61.0, 42.7, 109.0, 90.7);
394 395 396

    painterTester.assertExpectations();
    semantics.dispose();
397
  });
398 399

  testWidgets('provides semantics information for minutes', (WidgetTester tester) async {
400
    final SemanticsTester semantics = SemanticsTester(tester);
401
    await mediaQueryBoilerplate(tester, true);
402
    await tester.tap(_minuteControl);
403 404 405 406
    await tester.pumpAndSettle();

    final CustomPaint dialPaint = tester.widget(find.byKey(const ValueKey<String>('time-picker-dial')));
    final CustomPainter dialPainter = dialPaint.painter;
407
    final _CustomPainterSemanticsTester painterTester = _CustomPainterSemanticsTester(tester, dialPainter, semantics);
408

409 410 411 412 413 414 415 416 417 418 419 420
    painterTester.addLabel('00', 86.0, 0.0, 134.0, 48.0);
    painterTester.addLabel('05', 129.0, 11.5, 177.0, 59.5);
    painterTester.addLabel('10', 160.5, 43.0, 208.5, 91.0);
    painterTester.addLabel('15', 172.0, 86.0, 220.0, 134.0);
    painterTester.addLabel('20', 160.5, 129.0, 208.5, 177.0);
    painterTester.addLabel('25', 129.0, 160.5, 177.0, 208.5);
    painterTester.addLabel('30', 86.0, 172.0, 134.0, 220.0);
    painterTester.addLabel('35', 43.0, 160.5, 91.0, 208.5);
    painterTester.addLabel('40', 11.5, 129.0, 59.5, 177.0);
    painterTester.addLabel('45', 0.0, 86.0, 48.0, 134.0);
    painterTester.addLabel('50', 11.5, 43.0, 59.5, 91.0);
    painterTester.addLabel('55', 43.0, 11.5, 91.0, 59.5);
421 422 423 424 425 426 427 428 429 430

    painterTester.assertExpectations();
    semantics.dispose();
  });

  testWidgets('picks the right dial ring from widget configuration', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 12, minute: 0));
    dynamic dialPaint = tester.widget(findDialPaint);
    expect('${dialPaint.painter.activeRing}', '_DialRing.inner');

431
    await tester.pumpWidget(Container()); // make sure previous state isn't reused
432 433 434 435 436

    await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 0, minute: 0));
    dialPaint = tester.widget(findDialPaint);
    expect('${dialPaint.painter.activeRing}', '_DialRing.outer');
  });
437 438

  testWidgets('can increment and decrement hours', (WidgetTester tester) async {
439
    final SemanticsTester semantics = SemanticsTester(tester);
440

441
    Future<void> actAndExpect({ String initialValue, SemanticsAction action, String finalValue }) async {
442 443 444 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 471 472 473 474 475
      final SemanticsNode elevenHours = semantics.nodesWith(
        value: initialValue,
        ancestor: tester.renderObject(_hourControl).debugSemantics,
      ).single;
      tester.binding.pipelineOwner.semanticsOwner.performAction(elevenHours.id, action);
      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.
    final dynamic pickerState = tester.state(_timePickerDialog);
    expect(pickerState.selectedTime, const TimeOfDay(hour: 1, minute: 0));

    await actAndExpect(
      initialValue: '1',
      action: SemanticsAction.decrease,
      finalValue: '12',
    );
476
    await tester.pumpWidget(Container()); // clear old boilerplate
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499

    // 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',
    );
500 501

    semantics.dispose();
502 503 504
  });

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

507
    Future<void> actAndExpect({ String initialValue, SemanticsAction action, String finalValue }) async {
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
      final SemanticsNode elevenHours = semantics.nodesWith(
        value: initialValue,
        ancestor: tester.renderObject(_minuteControl).debugSemantics,
      ).single;
      tester.binding.pipelineOwner.semanticsOwner.performAction(elevenHours.id, action);
      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.
    final dynamic pickerState = tester.state(_timePickerDialog);
    expect(pickerState.selectedTime, const TimeOfDay(hour: 11, minute: 0));

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

    semantics.dispose();
548
  });
549

550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
  testWidgets('header touch regions are large enough', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false);

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

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

    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));
  });

582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
  testWidgets('builder parameter', (WidgetTester tester) async {
    Widget buildFrame(TextDirection textDirection) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return RaisedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showTimePicker(
                      context: context,
                      initialTime: const TimeOfDay(hour: 7, minute: 0),
                      builder: (BuildContext context, Widget child) {
                        return Directionality(
                          textDirection: textDirection,
                          child: child,
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    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);
  });
629 630 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 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695

  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) {
              return RaisedButton(
                onPressed: () {
                  showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(RaisedButton));

    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) {
              return RaisedButton(
                onPressed: () {
                  showTimePicker(
                    context: context,
                    useRootNavigator: false,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(RaisedButton));

    expect(rootObserver.pickerCount, 0);
    expect(nestedObserver.pickerCount, 1);
  });
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742

  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),
    );
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    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),
    );
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(tester.getSize(find.text('AM')).height, equals(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),
    );
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(tester.getSize(find.text('AM')).height, equals(amHeight * 2));
  });
743 744 745 746 747 748 749 750
}

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

class _SemanticsNodeExpectation {
751 752
  _SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);

753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
  final String label;
  final double left;
  final double top;
  final double right;
  final double bottom;
}

class _CustomPainterSemanticsTester {
  _CustomPainterSemanticsTester(this.tester, this.painter, this.semantics);

  final WidgetTester tester;
  final CustomPainter painter;
  final SemanticsTester semantics;
  final PaintPattern expectedLabels = paints;
  final List<_SemanticsNodeExpectation> expectedNodes = <_SemanticsNodeExpectation>[];

  void addLabel(String label, double left, double top, double right, double bottom) {
770
    expectedNodes.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
771 772 773
  }

  void assertExpectations() {
774
    final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
775 776 777 778 779 780
    painter.paint(canvasRecording, const Size(220.0, 220.0));
    final List<ui.Paragraph> paragraphs = canvasRecording.invocations
      .where((RecordedInvocation recordedInvocation) {
        return recordedInvocation.invocation.memberName == #drawParagraph;
      })
      .map<ui.Paragraph>((RecordedInvocation recordedInvocation) {
781
        return recordedInvocation.invocation.positionalArguments.first as ui.Paragraph;
782 783 784 785 786 787
      })
      .toList();

    final PaintPattern expectedLabels = paints;
    int i = 0;

788
    for (final _SemanticsNodeExpectation expectation in expectedNodes) {
789
      expect(semantics, includesNodeWith(value: expectation.label));
790
      final Iterable<SemanticsNode> dialLabelNodes = semantics
791
          .nodesWith(value: expectation.label)
792 793
          .where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
      expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
794
      final Rect rect = Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
      expect(dialLabelNodes.single.rect, within(distance: 1.0, from: rect),
        reason: 'This is checking the node rectangle for label ${expectation.label}');

      final ui.Paragraph paragraph = paragraphs[i++];

      // The label text paragraph and the semantics node share the same center,
      // but have different sizes.
      final Offset center = dialLabelNodes.single.rect.center;
      final Offset topLeft = center.translate(
        -paragraph.width / 2.0,
        -paragraph.height / 2.0,
      );

      expectedLabels.paragraph(
        paragraph: paragraph,
        offset: within<Offset>(distance: 1.0, from: topLeft),
      );
    }
    expect(tester.renderObject(findDialPaint), expectedLabels);
  }
Hans Muller's avatar
Hans Muller committed
815
}
816 817 818 819 820 821 822 823 824 825 826 827

class PickerObserver extends NavigatorObserver {
  int pickerCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    if (route.toString().contains('_DialogRoute')) {
      pickerCount++;
    }
    super.didPush(route, previousRoute);
  }
}