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

5
import 'dart:async';
6
import 'dart:ui' as ui;
7

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

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

18 19 20 21
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');

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

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

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

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

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

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

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

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

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

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

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

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

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

107 108 109 110 111
    TestGesture gesture;

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

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

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

129
    expect(await startPicker(tester, (TimeOfDay time) { result = time; }), equals(center));
130 131 132
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
133 134
    await finishPicker(tester);
    expect(result.hour, equals(9));
Hans Muller's avatar
Hans Muller committed
135
  });
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
  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)));
  });

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

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

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

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

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

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

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

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

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

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

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

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

255 256 257
  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'];
258

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

502
    Future<void> actAndExpect({ String initialValue, SemanticsAction action, String finalValue }) async {
503 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
      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',
    );
541 542

    semantics.dispose();
543
  });
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 578 579 580 581 582 583 584 585 586 587 588 589 590 591

  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);
  });
592 593 594 595 596 597 598 599
}

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

class _SemanticsNodeExpectation {
600 601
  _SemanticsNodeExpectation(this.label, this.left, this.top, this.right, this.bottom);

602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
  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) {
619
    expectedNodes.add(_SemanticsNodeExpectation(label, left, top, right, bottom));
620 621 622
  }

  void assertExpectations() {
623
    final TestRecordingCanvas canvasRecording = TestRecordingCanvas();
624 625 626 627 628 629 630 631 632 633 634 635 636 637
    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) {
        return recordedInvocation.invocation.positionalArguments.first;
      })
      .toList();

    final PaintPattern expectedLabels = paints;
    int i = 0;

    for (_SemanticsNodeExpectation expectation in expectedNodes) {
638
      expect(semantics, includesNodeWith(value: expectation.label));
639
      final Iterable<SemanticsNode> dialLabelNodes = semantics
640
          .nodesWith(value: expectation.label)
641 642
          .where((SemanticsNode node) => node.tags?.contains(const SemanticsTag('dial-label')) ?? false);
      expect(dialLabelNodes, hasLength(1), reason: 'Expected exactly one label ${expectation.label}');
643
      final Rect rect = Rect.fromLTRB(expectation.left, expectation.top, expectation.right, expectation.bottom);
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
      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
664
}