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

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
297 298 299 300 301
  }

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

302
    final CustomPaint dialPaint = tester.widget(findDialPaint);
303
    final dynamic dialPainter = dialPaint.painter;
304 305
    final List<dynamic> primaryOuterLabels = dialPainter.primaryOuterLabels as List<dynamic>;
    expect(primaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
306 307
    expect(dialPainter.primaryInnerLabels, null);

308 309
    final List<dynamic> secondaryOuterLabels = dialPainter.secondaryOuterLabels as List<dynamic>;
    expect(secondaryOuterLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
310 311 312 313 314 315
    expect(dialPainter.secondaryInnerLabels, null);
  });

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

316
    final CustomPaint dialPaint = tester.widget(findDialPaint);
317
    final dynamic dialPainter = dialPaint.painter;
318 319 320 321 322 323 324 325 326
    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);
327 328 329
  });

  testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
330
    final SemanticsTester semantics = SemanticsTester(tester);
331 332 333 334 335 336 337 338 339
    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 {
340
    final SemanticsTester semantics = SemanticsTester(tester);
341 342 343
    await mediaQueryBoilerplate(tester, true);

    expect(semantics, isNot(includesNodeWith(label: ':')));
344
    expect(semantics.nodesWith(value: '00'), hasLength(2),
345
        reason: '00 appears once in the header, then again in the dial');
346
    expect(semantics.nodesWith(value: '07'), hasLength(2),
347 348 349 350 351 352 353 354 355 356 357 358
        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 {
359
    final SemanticsTester semantics = SemanticsTester(tester);
360 361 362 363
    await mediaQueryBoilerplate(tester, true);

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

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389
    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);
390 391 392

    painterTester.assertExpectations();
    semantics.dispose();
393
  });
394 395

  testWidgets('provides semantics information for minutes', (WidgetTester tester) async {
396
    final SemanticsTester semantics = SemanticsTester(tester);
397
    await mediaQueryBoilerplate(tester, true);
398
    await tester.tap(_minuteControl);
399 400 401 402
    await tester.pumpAndSettle();

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

405 406 407 408 409 410 411 412 413 414 415 416
    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);
417 418 419 420 421 422 423 424 425 426

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

427
    await tester.pumpWidget(Container()); // make sure previous state isn't reused
428 429 430 431 432

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

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

437
    Future<void> actAndExpect({ String initialValue, SemanticsAction action, String finalValue }) async {
438 439 440 441 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
      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',
    );
472
    await tester.pumpWidget(Container()); // clear old boilerplate
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495

    // 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',
    );
496 497

    semantics.dispose();
498 499 500
  });

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

503
    Future<void> actAndExpect({ String initialValue, SemanticsAction action, String finalValue }) async {
504 505 506 507 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
      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',
    );
542 543

    semantics.dispose();
544
  });
545

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

578 579 580 581 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
  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);
  });
625 626 627 628 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

  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);
  });
692 693 694 695 696 697 698 699
}

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

class _SemanticsNodeExpectation {
700 701
  _SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);

702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
  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) {
719
    expectedNodes.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
720 721 722
  }

  void assertExpectations() {
723
    final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
724 725 726 727 728 729
    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) {
730
        return recordedInvocation.invocation.positionalArguments.first as ui.Paragraph;
731 732 733 734 735 736 737
      })
      .toList();

    final PaintPattern expectedLabels = paints;
    int i = 0;

    for (_SemanticsNodeExpectation expectation in expectedNodes) {
738
      expect(semantics, includesNodeWith(value: expectation.label));
739
      final Iterable<SemanticsNode> dialLabelNodes = semantics
740
          .nodesWith(value: expectation.label)
741 742
          .where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
      expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
743
      final Rect rect = Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
      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
764
}
765 766 767 768 769 770 771 772 773 774 775 776

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