widget_tester_test.dart 25.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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:io';
7
import 'dart:ui';
8

9
import 'package:flutter/foundation.dart';
10 11
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
12
import 'package:flutter/rendering.dart';
13
import 'package:flutter/scheduler.dart';
14
import 'package:flutter_test/flutter_test.dart';
15 16

// ignore: deprecated_member_use
17 18
import 'package:test_api/test_api.dart' as test_package;
import 'package:test_api/src/frontend/async_matcher.dart' show AsyncMatcher;
19

20 21 22
const List<Widget> fooBarTexts = <Text>[
  Text('foo', textDirection: TextDirection.ltr),
  Text('bar', textDirection: TextDirection.ltr),
Ian Hickson's avatar
Ian Hickson committed
23 24
];

25
void main() {
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
  group('getSemanticsData', () {
    testWidgets('throws when there are no semantics', (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: Text('hello'),
          ),
        ),
      );

      expect(() => tester.getSemantics(find.text('hello')),
        throwsA(isInstanceOf<StateError>()));
    }, semanticsEnabled: false);

    testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async {
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Row(
              children: const <Widget>[
                Text('hello'),
                Text('hello'),
              ],
            ),
          ),
        ),
      );

      expect(() => tester.getSemantics(find.text('hello')),
          throwsA(isInstanceOf<StateError>()));
      semanticsHandle.dispose();
    });

    testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Container(
              child: OutlineButton(
                  onPressed: () { },
                  child: const Text('hello'),
              ),
            ),
          ),
        ),
      );

      final SemanticsNode node = tester.getSemantics(find.text('hello'));
      final SemanticsData semantics = node.getSemanticsData();
      expect(semantics.label, 'hello');
      expect(semantics.hasAction(SemanticsAction.tap), true);
      expect(semantics.hasFlag(SemanticsFlag.isButton), true);
      semanticsHandle.dispose();
    });

    testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Container(
              child: OutlineButton(
                  onPressed: () { },
                  child: const Text('hello'),
              ),
            ),
          ),
        ),
      );

      final SemanticsNode node = tester.getSemantics(find.text('hello'));
      final SemanticsData semantics = node.getSemanticsData();
      expect(semantics.label, 'hello');
      expect(semantics.hasAction(SemanticsAction.tap), true);
      expect(semantics.hasFlag(SemanticsFlag.isButton), true);
    }, semanticsEnabled: true);

    testWidgets('Returns merged SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();
      const Key key = Key('test');
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Semantics(
              label: 'A',
              child: Semantics(
                label: 'B',
                child: Semantics(
                  key: key,
                  label: 'C',
                  child: Container(),
                ),
              ),
            ),
          ),
        ),
      );

      final SemanticsNode node = tester.getSemantics(find.byKey(key));
      final SemanticsData semantics = node.getSemanticsData();
      expect(semantics.label, 'A\nB\nC');
      semanticsHandle.dispose();
    });
  });

  group('ensureVisible', () {
    testWidgets('scrolls to make widget visible', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: ListView.builder(
              itemCount: 20,
              shrinkWrap: true,
              itemBuilder: (BuildContext context, int i) => ListTile(title: Text('Item $i')),
           ),
         ),
        ),
      );

      // Make sure widget isn't on screen
      expect(find.text('Item 15', skipOffstage: true), findsNothing);

      await tester.ensureVisible(find.text('Item 15', skipOffstage: false));
      await tester.pumpAndSettle();

      expect(find.text('Item 15', skipOffstage: true), findsOneWidget);
    });
  });

158 159
  group('expectLater', () {
    testWidgets('completes when matcher completes', (WidgetTester tester) async {
160 161
      final Completer<void> completer = Completer<void>();
      final Future<void> future = expectLater(null, FakeMatcher(completer));
162 163 164
      String result;
      future.then<void>((void value) {
        result = '123';
165
      });
166
      test_package.expect(result, isNull);
167
      completer.complete();
168
      test_package.expect(result, isNull);
169 170
      await future;
      await tester.pump();
171
      test_package.expect(result, '123');
172
    });
173 174

    testWidgets('respects the skip flag', (WidgetTester tester) async {
175 176
      final Completer<void> completer = Completer<void>();
      final Future<void> future = expectLater(null, FakeMatcher(completer), skip: 'testing skip');
177
      bool completed = false;
178
      future.then<void>((_) {
179 180 181 182 183
        completed = true;
      });
      test_package.expect(completed, isFalse);
      await future;
      test_package.expect(completed, isTrue);
184
    });
185 186
  });

187
  group('findsOneWidget', () {
188
    testWidgets('finds exactly one widget', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
189
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
190
      expect(find.text('foo'), findsOneWidget);
191 192
    });

193
    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
194 195
      TestFailure failure;
      try {
196
        expect(find.text('foo', skipOffstage: false), findsOneWidget);
197
      } on TestFailure catch (e) {
198 199 200 201
        failure = e;
      }

      expect(failure, isNotNull);
202
      final String message = failure.message;
203
      expect(message, contains('Expected: exactly one matching node in the widget tree\n'));
204
      expect(message, contains('Actual: _TextFinder:<zero widgets with text "foo">\n'));
205
      expect(message, contains('Which: means none were found but one was expected\n'));
206 207 208
    });
  });

209
  group('findsNothing', () {
210
    testWidgets('finds no widgets', (WidgetTester tester) async {
211
      expect(find.text('foo'), findsNothing);
212 213
    });

214
    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
215
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
216 217 218

      TestFailure failure;
      try {
219
        expect(find.text('foo', skipOffstage: false), findsNothing);
220
      } on TestFailure catch (e) {
221 222 223 224
        failure = e;
      }

      expect(failure, isNotNull);
225
      final String message = failure.message;
226 227

      expect(message, contains('Expected: no matching nodes in the widget tree\n'));
228
      expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo": Text("foo", textDirection: ltr)>\n'));
229
      expect(message, contains('Which: means one was found but none were expected\n'));
230
    });
231 232

    testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
233
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
234 235 236 237

      TestFailure failure;
      try {
        expect(find.text('foo'), findsNothing);
238
      } on TestFailure catch (e) {
239 240 241 242
        failure = e;
      }

      expect(failure, isNotNull);
243
      final String message = failure.message;
244 245

      expect(message, contains('Expected: no matching nodes in the widget tree\n'));
246
      expect(message, contains('Actual: _TextFinder:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo", textDirection: ltr)>\n'));
247 248
      expect(message, contains('Which: means one was found but none were expected\n'));
    });
249 250

    testWidgets('pumping', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
251
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
252 253
      int count;

254
      final AnimationController test = AnimationController(
255 256 257
        duration: const Duration(milliseconds: 5100),
        vsync: tester,
      );
258 259
      count = await tester.pumpAndSettle(const Duration(seconds: 1));
      expect(count, 1); // it always pumps at least one frame
260 261

      test.forward(from: 0.0);
262
      count = await tester.pumpAndSettle(const Duration(seconds: 1));
263 264 265 266 267 268 269 270 271 272 273
      // 1 frame at t=0, starting the animation
      // 1 frame at t=1
      // 1 frame at t=2
      // 1 frame at t=3
      // 1 frame at t=4
      // 1 frame at t=5
      // 1 frame at t=6, ending the animation
      expect(count, 7);

      test.forward(from: 0.0);
      await tester.pump(); // starts the animation
274
      count = await tester.pumpAndSettle(const Duration(seconds: 1));
275 276 277 278 279
      expect(count, 6);

      test.forward(from: 0.0);
      await tester.pump(); // starts the animation
      await tester.pump(); // has no effect
280
      count = await tester.pumpAndSettle(const Duration(seconds: 1));
281 282
      expect(count, 6);
    });
283
  });
284

285 286
  group('find.byElementPredicate', () {
    testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
287
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
288

289
      const String customDescription = 'custom description';
290 291 292
      TestFailure failure;
      try {
        expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget);
293
      } on TestFailure catch (e) {
294 295 296 297
        failure = e;
      }

      expect(failure, isNotNull);
298
      expect(failure.message, contains('Actual: _ElementPredicateFinder:<zero widgets with $customDescription'));
299 300 301 302 303
    });
  });

  group('find.byWidgetPredicate', () {
    testWidgets('fails with a custom description in the message', (WidgetTester tester) async {
Ian Hickson's avatar
Ian Hickson committed
304
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
305

306
      const String customDescription = 'custom description';
307 308 309
      TestFailure failure;
      try {
        expect(find.byWidgetPredicate((_) => false, description: customDescription), findsOneWidget);
310
      } on TestFailure catch (e) {
311 312 313 314
        failure = e;
      }

      expect(failure, isNotNull);
315
      expect(failure.message, contains('Actual: _WidgetPredicateFinder:<zero widgets with $customDescription'));
316 317
    });
  });
318 319 320

  group('find.descendant', () {
    testWidgets('finds one descendant', (WidgetTester tester) async {
321
      await tester.pumpWidget(Row(
322 323
        textDirection: TextDirection.ltr,
        children: <Widget>[
324
          Column(children: fooBarTexts),
325 326
        ],
      ));
327 328 329

      expect(find.descendant(
        of: find.widgetWithText(Row, 'foo'),
330
        matching: find.text('bar'),
331 332 333 334
      ), findsOneWidget);
    });

    testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async {
335
      await tester.pumpWidget(Row(
336 337
        textDirection: TextDirection.ltr,
        children: <Widget>[
338 339
          Column(children: fooBarTexts),
          Column(children: fooBarTexts),
340 341
        ],
      ));
342 343 344

      expect(find.descendant(
        of: find.widgetWithText(Column, 'foo'),
345
        matching: find.text('bar'),
346 347 348 349
      ), findsNWidgets(2));
    });

    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
350
      await tester.pumpWidget(Row(
351 352
        textDirection: TextDirection.ltr,
        children: <Widget>[
353
          Column(children: const <Text>[Text('foo', textDirection: TextDirection.ltr)]),
Ian Hickson's avatar
Ian Hickson committed
354
          const Text('bar', textDirection: TextDirection.ltr),
355 356
        ],
      ));
357 358 359 360 361

      TestFailure failure;
      try {
        expect(find.descendant(
          of: find.widgetWithText(Column, 'foo'),
362
          matching: find.text('bar'),
363
        ), findsOneWidget);
364
      } on TestFailure catch (e) {
365 366 367 368 369
        failure = e;
      }

      expect(failure, isNotNull);
      expect(
370
        failure.message,
371
        contains(
372
          'Actual: _DescendantFinder:<zero widgets with text "bar" that has ancestor(s) with type "Column" which is an ancestor of text "foo"',
373
        ),
374
      );
375
    });
376
  });
377

378 379
  group('find.ancestor', () {
    testWidgets('finds one ancestor', (WidgetTester tester) async {
380
      await tester.pumpWidget(Row(
381 382
        textDirection: TextDirection.ltr,
        children: <Widget>[
383
          Column(children: fooBarTexts),
384 385
        ],
      ));
386

387 388 389 390 391 392 393 394
      expect(find.ancestor(
        of: find.text('bar'),
        matching: find.widgetWithText(Row, 'foo'),
      ), findsOneWidget);
    });

    testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async {
      await tester.pumpWidget(
395
        Directionality(
396
          textDirection: TextDirection.ltr,
397
          child: Row(
398
            children: <Widget>[
399
              Row(children: fooBarTexts),
400 401 402 403 404 405 406
            ],
          ),
        ),
      );

      expect(find.ancestor(
        of: find.text('bar'),
407
        matching: find.byType(Row),
408 409 410 411
      ), findsNWidgets(2));
    });

    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
412
      await tester.pumpWidget(Row(
413 414
        textDirection: TextDirection.ltr,
        children: <Widget>[
415
          Column(children: const <Text>[Text('foo', textDirection: TextDirection.ltr)]),
416 417 418 419 420 421 422 423 424 425
          const Text('bar', textDirection: TextDirection.ltr),
        ],
      ));

      TestFailure failure;
      try {
        expect(find.ancestor(
          of: find.text('bar'),
          matching: find.widgetWithText(Column, 'foo'),
        ), findsOneWidget);
426
      } on TestFailure catch (e) {
427 428 429 430 431 432
        failure = e;
      }

      expect(failure, isNotNull);
      expect(
        failure.message,
433
        contains(
434
          'Actual: _AncestorFinder:<zero widgets with type "Column" which is an ancestor of text "foo" which is an ancestor of text "bar"',
435
        ),
436 437 438 439
      );
    });

    testWidgets('Root not matched by default', (WidgetTester tester) async {
440
      await tester.pumpWidget(Row(
441 442
        textDirection: TextDirection.ltr,
        children: <Widget>[
443
          Column(children: fooBarTexts),
444 445 446 447 448 449
        ],
      ));

      expect(find.ancestor(
        of: find.byType(Column),
        matching: find.widgetWithText(Column, 'foo'),
450 451 452 453
      ), findsNothing);
    });

    testWidgets('Match the root', (WidgetTester tester) async {
454
      await tester.pumpWidget(Row(
455 456
        textDirection: TextDirection.ltr,
        children: <Widget>[
457
          Column(children: fooBarTexts),
458 459
        ],
      ));
460 461

      expect(find.descendant(
462 463
        of: find.byType(Column),
        matching: find.widgetWithText(Column, 'foo'),
464 465 466
        matchRoot: true,
      ), findsOneWidget);
    });
467
  });
468

469
  group('pageBack', () {
470
    testWidgets('fails when there are no back buttons', (WidgetTester tester) async {
471
      await tester.pumpWidget(Container());
472 473 474

      expect(
        expectAsync0(tester.pageBack),
475
        throwsA(isInstanceOf<TestFailure>()),
476 477 478 479 480
      );
    });

    testWidgets('successfully taps material back buttons', (WidgetTester tester) async {
      await tester.pumpWidget(
481 482 483
        MaterialApp(
          home: Center(
            child: Builder(
484
              builder: (BuildContext context) {
485
                return RaisedButton(
486 487
                  child: const Text('Next'),
                  onPressed: () {
488
                    Navigator.push<void>(context, MaterialPageRoute<void>(
489
                      builder: (BuildContext context) {
490 491
                        return Scaffold(
                          appBar: AppBar(
492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
                            title: const Text('Page 2'),
                          ),
                        );
                      },
                    ));
                  },
                );
              } ,
            ),
          ),
        ),
      );

      await tester.tap(find.text('Next'));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 400));

      await tester.pageBack();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 400));

      expect(find.text('Next'), findsOneWidget);
      expect(find.text('Page 2'), findsNothing);
    });

    testWidgets('successfully taps cupertino back buttons', (WidgetTester tester) async {
      await tester.pumpWidget(
519 520 521
        MaterialApp(
          home: Center(
            child: Builder(
522
              builder: (BuildContext context) {
523
                return CupertinoButton(
524 525
                  child: const Text('Next'),
                  onPressed: () {
526
                    Navigator.push<void>(context, CupertinoPageRoute<void>(
527
                      builder: (BuildContext context) {
528
                        return CupertinoPageScaffold(
529
                          navigationBar: const CupertinoNavigationBar(
530
                            middle: Text('Page 2'),
531
                          ),
532
                          child: Container(),
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
                        );
                      },
                    ));
                  },
                );
              } ,
            ),
          ),
        ),
      );

      await tester.tap(find.text('Next'));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 400));

      await tester.pageBack();
      await tester.pump();
550
      await tester.pumpAndSettle();
551 552 553 554 555 556

      expect(find.text('Next'), findsOneWidget);
      expect(find.text('Page 2'), findsNothing);
    });
  });

557
  testWidgets('hasRunningAnimations control test', (WidgetTester tester) async {
558
    final AnimationController controller = AnimationController(
559
      duration: const Duration(seconds: 1),
560
      vsync: const TestVSync(),
561 562 563 564 565 566 567 568 569 570 571
    );
    expect(tester.hasRunningAnimations, isFalse);
    controller.forward();
    expect(tester.hasRunningAnimations, isTrue);
    controller.stop();
    expect(tester.hasRunningAnimations, isFalse);
    controller.forward();
    expect(tester.hasRunningAnimations, isTrue);
    await tester.pumpAndSettle();
    expect(tester.hasRunningAnimations, isFalse);
  });
572 573

  testWidgets('pumpAndSettle control test', (WidgetTester tester) async {
574
    final AnimationController controller = AnimationController(
575
      duration: const Duration(minutes: 525600),
576
      vsync: const TestVSync(),
577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
    );
    expect(await tester.pumpAndSettle(), 1);
    controller.forward();
    try {
      await tester.pumpAndSettle();
      expect(true, isFalse);
    } catch (e) {
      expect(e, isFlutterError);
    }
    controller.stop();
    expect(await tester.pumpAndSettle(), 1);
    controller.duration = const Duration(seconds: 1);
    controller.forward();
    expect(await tester.pumpAndSettle(const Duration(milliseconds: 300)), 5); // 0, 300, 600, 900, 1200ms
  });
592 593 594 595 596 597 598 599 600 601 602

  group('runAsync', () {
    testWidgets('works with no async calls', (WidgetTester tester) async {
      String value;
      await tester.runAsync(() async {
        value = '123';
      });
      expect(value, '123');
    });

    testWidgets('works with real async calls', (WidgetTester tester) async {
603
      final StringBuffer buf = StringBuffer('1');
604 605
      await tester.runAsync(() async {
        buf.write('2');
606
        //ignore: avoid_slow_async_io
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
        await Directory.current.stat();
        buf.write('3');
      });
      buf.write('4');
      expect(buf.toString(), '1234');
    });

    testWidgets('propagates return values', (WidgetTester tester) async {
      final String value = await tester.runAsync<String>(() async {
        return '123';
      });
      expect(value, '123');
    });

    testWidgets('reports errors via framework', (WidgetTester tester) async {
      final String value = await tester.runAsync<String>(() async {
623
        throw ArgumentError();
624 625 626 627 628 629
      });
      expect(value, isNull);
      expect(tester.takeException(), isArgumentError);
    });

    testWidgets('disallows re-entry', (WidgetTester tester) async {
630
      final Completer<void> completer = Completer<void>();
631
      tester.runAsync<void>(() => completer.future);
632
      expect(() => tester.runAsync(() async { }), throwsA(isInstanceOf<TestFailure>()));
633 634
      completer.complete();
    });
635 636

    testWidgets('maintains existing zone values', (WidgetTester tester) async {
637
      final Object key = Object();
638
      await runZoned<Future<void>>(() {
639
        expect(Zone.current[key], 'abczed');
640
        return tester.runAsync<void>(() async {
641 642 643 644 645 646
          expect(Zone.current[key], 'abczed');
        });
      }, zoneValues: <dynamic, dynamic>{
        key: 'abczed',
      });
    });
647
  });
648 649 650

  testWidgets('showKeyboard can be called twice', (WidgetTester tester) async {
    await tester.pumpWidget(
651 652 653 654
      MaterialApp(
        home: Material(
          child: Center(
            child: TextFormField(),
655 656 657 658 659
          ),
        ),
      ),
    );
    await tester.showKeyboard(find.byType(TextField));
660
    await tester.testTextInput.receiveAction(TextInputAction.done);
661 662
    await tester.pump();
    await tester.showKeyboard(find.byType(TextField));
663
    await tester.testTextInput.receiveAction(TextInputAction.done);
664 665 666 667 668
    await tester.pump();
    await tester.showKeyboard(find.byType(TextField));
    await tester.showKeyboard(find.byType(TextField));
    await tester.pump();
  });
669

670
  testWidgets('verifyTickersWereDisposed control test', (WidgetTester tester) async {
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 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 743 744 745
    FlutterError error;
    final Ticker ticker = tester.createTicker((Duration duration) {});
    ticker.start();
    try {
      tester.verifyTickersWereDisposed('');
    } on FlutterError catch (e) {
      error = e;
    } finally {
      expect(error, isNotNull);
      expect(error.diagnostics.length, 4);
      expect(error.diagnostics[2].level, DiagnosticLevel.hint);
      expect(
        error.diagnostics[2].toStringDeep(),
        'Tickers used by AnimationControllers should be disposed by\n'
        'calling dispose() on the AnimationController itself. Otherwise,\n'
        'the ticker will leak.\n',
      );
      expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<Ticker>>());
      expect(error.diagnostics.last.value, ticker);
      expect(error.toStringDeep(), startsWith(
        'FlutterError\n'
        '   A Ticker was active .\n'
        '   All Tickers must be disposed.\n'
        '   Tickers used by AnimationControllers should be disposed by\n'
        '   calling dispose() on the AnimationController itself. Otherwise,\n'
        '   the ticker will leak.\n'
        '   The offending ticker was:\n'
        '     _TestTicker()\n',
      ));
    }
    ticker.stop();
  });

  group('testWidgets variants work', () {
    int numberOfVariationsRun = 0;

    testWidgets('variant tests run all values provided', (WidgetTester tester) async {
      if (debugDefaultTargetPlatformOverride == null) {
        expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
      } else {
        numberOfVariationsRun += 1;
      }
    }, variant: TargetPlatformVariant(TargetPlatform.values.toSet()));

    testWidgets('variant tests have descriptions with details', (WidgetTester tester) async {
      if (debugDefaultTargetPlatformOverride == null) {
        expect(tester.testDescription, equals('variant tests have descriptions with details'));
      } else {
        expect(tester.testDescription, equals('variant tests have descriptions with details ($debugDefaultTargetPlatformOverride)'));
      }
    }, variant: TargetPlatformVariant(TargetPlatform.values.toSet()));
  });

  group('TargetPlatformVariant', () {
    int numberOfVariationsRun = 0;
    TargetPlatform origTargetPlatform;

    setUpAll((){
      origTargetPlatform = debugDefaultTargetPlatformOverride;
    });

    tearDownAll((){
      expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform));
    });

    testWidgets('TargetPlatformVariant.only tests given value', (WidgetTester tester) async {
      expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS));
      expect(defaultTargetPlatform, equals(TargetPlatform.iOS));
    }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));

    testWidgets('TargetPlatformVariant.all tests run all variants', (WidgetTester tester) async {
      if (debugDefaultTargetPlatformOverride == null) {
        expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
      } else {
        numberOfVariationsRun += 1;
746
      }
747
    }, variant: TargetPlatformVariant.all());
748 749
  });

750
}
751 752 753 754 755 756 757 758

class FakeMatcher extends AsyncMatcher {
  FakeMatcher(this.completer);

  final Completer<void> completer;

  @override
  Future<String> matchAsync(dynamic object) {
759
    return completer.future.then<String>((void value) {
760 761 762 763 764 765 766
      return object?.toString();
    });
  }

  @override
  Description describe(Description description) => description.add('--fake--');
}
767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791

class _SingleTickerTest extends StatefulWidget {
  const _SingleTickerTest({Key key}) : super(key: key);

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

class _SingleTickerTestState extends State<_SingleTickerTest> with SingleTickerProviderStateMixin {
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 100),
    )  ;
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}