1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:io';
7
8import 'package:flutter/cupertino.dart';
9import 'package:flutter/foundation.dart';
10import 'package:flutter/gestures.dart';
11import 'package:flutter/material.dart';
12import 'package:flutter/rendering.dart';
13import 'package:flutter/scheduler.dart';
14import 'package:flutter/services.dart';
15import 'package:flutter_test/flutter_test.dart';
16import 'package:matcher/expect.dart' as matcher;
17import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports
18
19import 'multi_view_testing.dart';
20
21void main() {
22 group('expectLater', () {
23 testWidgets('completes when matcher completes', (WidgetTester tester) async {
24 final Completer<void> completer = Completer<void>();
25 final Future<void> future = expectLater(null, FakeMatcher(completer));
26 String? result;
27 future.then<void>((void value) {
28 result = '123';
29 });
30 matcher.expect(result, isNull);
31 completer.complete();
32 matcher.expect(result, isNull);
33 await future;
34 await tester.pump();
35 matcher.expect(result, '123');
36 });
37
38 testWidgets('respects the skip flag', (WidgetTester tester) async {
39 final Completer<void> completer = Completer<void>();
40 final Future<void> future = expectLater(null, FakeMatcher(completer), skip: 'testing skip'); // [intended] API testing
41 bool completed = false;
42 future.then<void>((_) {
43 completed = true;
44 });
45 matcher.expect(completed, isFalse);
46 await future;
47 matcher.expect(completed, isTrue);
48 });
49 });
50
51 group('group retry flag allows test to run multiple times', () {
52 bool retried = false;
53 group('the group with retry flag', () {
54 testWidgets('the test inside it', (WidgetTester tester) async {
55 addTearDown(() => retried = true);
56 if (!retried) {
57 debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
58 }
59 expect(retried, isTrue);
60 });
61 }, retry: 1);
62 });
63
64 group('testWidget retry flag allows test to run multiple times', () {
65 bool retried = false;
66 testWidgets('the test with retry flag', (WidgetTester tester) async {
67 addTearDown(() => retried = true);
68 if (!retried) {
69 debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED');
70 }
71 expect(retried, isTrue);
72 }, retry: 1);
73 });
74
75 group('respects the group skip flag', () {
76 testWidgets('should be skipped', (WidgetTester tester) async {
77 expect(false, true);
78 });
79 }, skip: true); // [intended] API testing
80
81 group('pumping', () {
82 testWidgets('pumping', (WidgetTester tester) async {
83 await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
84 int count;
85
86 final AnimationController test = AnimationController(
87 duration: const Duration(milliseconds: 5100),
88 vsync: tester,
89 );
90 count = await tester.pumpAndSettle(const Duration(seconds: 1));
91 expect(count, 1); // it always pumps at least one frame
92
93 test.forward(from: 0.0);
94 count = await tester.pumpAndSettle(const Duration(seconds: 1));
95 // 1 frame at t=0, starting the animation
96 // 1 frame at t=1
97 // 1 frame at t=2
98 // 1 frame at t=3
99 // 1 frame at t=4
100 // 1 frame at t=5
101 // 1 frame at t=6, ending the animation
102 expect(count, 7);
103
104 test.forward(from: 0.0);
105 await tester.pump(); // starts the animation
106 count = await tester.pumpAndSettle(const Duration(seconds: 1));
107 expect(count, 6);
108
109 test.forward(from: 0.0);
110 await tester.pump(); // starts the animation
111 await tester.pump(); // has no effect
112 count = await tester.pumpAndSettle(const Duration(seconds: 1));
113 expect(count, 6);
114 });
115
116 testWidgets('pumpFrames', (WidgetTester tester) async {
117 final List<int> logPaints = <int>[];
118 int? initial;
119
120 final Widget target = _AlwaysAnimating(
121 onPaint: () {
122 final int current = SchedulerBinding.instance.currentFrameTimeStamp.inMicroseconds;
123 initial ??= current;
124 logPaints.add(current - initial!);
125 },
126 );
127
128 await tester.pumpFrames(target, const Duration(milliseconds: 55));
129
130 // `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump,
131 // so we expect 4 pumps of 16683 microseconds each in the 55ms duration.
132 expect(logPaints, <int>[0, 16683, 33366, 50049]);
133 logPaints.clear();
134
135 await tester.pumpFrames(target, const Duration(milliseconds: 30), const Duration(milliseconds: 10));
136
137 // Since `pumpFrames` was given a 10ms interval per pump, we expect the
138 // results to continue from 50049 with 10000 microseconds per pump over
139 // the 30ms duration.
140 expect(logPaints, <int>[60049, 70049, 80049]);
141 });
142 });
143 group('pageBack', () {
144 testWidgets('fails when there are no back buttons', (WidgetTester tester) async {
145 await tester.pumpWidget(Container());
146
147 expect(
148 expectAsync0(tester.pageBack),
149 throwsA(isA<TestFailure>()),
150 );
151 });
152
153 testWidgets('successfully taps material back buttons', (WidgetTester tester) async {
154 await tester.pumpWidget(
155 MaterialApp(
156 home: Center(
157 child: Builder(
158 builder: (BuildContext context) {
159 return ElevatedButton(
160 child: const Text('Next'),
161 onPressed: () {
162 Navigator.push<void>(context, MaterialPageRoute<void>(
163 builder: (BuildContext context) {
164 return Scaffold(
165 appBar: AppBar(
166 title: const Text('Page 2'),
167 ),
168 );
169 },
170 ));
171 },
172 );
173 } ,
174 ),
175 ),
176 ),
177 );
178
179 await tester.tap(find.text('Next'));
180 await tester.pump();
181 await tester.pump(const Duration(milliseconds: 400));
182
183 await tester.pageBack();
184 await tester.pump();
185 await tester.pump(const Duration(milliseconds: 400));
186
187 expect(find.text('Next'), findsOneWidget);
188 expect(find.text('Page 2'), findsNothing);
189 });
190
191 testWidgets('successfully taps cupertino back buttons', (WidgetTester tester) async {
192 await tester.pumpWidget(
193 MaterialApp(
194 home: Center(
195 child: Builder(
196 builder: (BuildContext context) {
197 return CupertinoButton(
198 child: const Text('Next'),
199 onPressed: () {
200 Navigator.push<void>(context, CupertinoPageRoute<void>(
201 builder: (BuildContext context) {
202 return CupertinoPageScaffold(
203 navigationBar: const CupertinoNavigationBar(
204 middle: Text('Page 2'),
205 ),
206 child: Container(),
207 );
208 },
209 ));
210 },
211 );
212 } ,
213 ),
214 ),
215 ),
216 );
217
218 await tester.tap(find.text('Next'));
219 await tester.pump();
220 await tester.pump(const Duration(milliseconds: 400));
221
222 await tester.pageBack();
223 await tester.pump();
224 await tester.pumpAndSettle();
225
226 expect(find.text('Next'), findsOneWidget);
227 expect(find.text('Page 2'), findsNothing);
228 });
229 });
230
231 testWidgets('hasRunningAnimations control test', (WidgetTester tester) async {
232 final AnimationController controller = AnimationController(
233 duration: const Duration(seconds: 1),
234 vsync: const TestVSync(),
235 );
236 expect(tester.hasRunningAnimations, isFalse);
237 controller.forward();
238 expect(tester.hasRunningAnimations, isTrue);
239 controller.stop();
240 expect(tester.hasRunningAnimations, isFalse);
241 controller.forward();
242 expect(tester.hasRunningAnimations, isTrue);
243 await tester.pumpAndSettle();
244 expect(tester.hasRunningAnimations, isFalse);
245 });
246
247 testWidgets('pumpAndSettle control test', (WidgetTester tester) async {
248 final AnimationController controller = AnimationController(
249 duration: const Duration(minutes: 525600),
250 vsync: const TestVSync(),
251 );
252 expect(await tester.pumpAndSettle(), 1);
253 controller.forward();
254 try {
255 await tester.pumpAndSettle();
256 expect(true, isFalse);
257 } catch (e) {
258 expect(e, isFlutterError);
259 }
260 controller.stop();
261 expect(await tester.pumpAndSettle(), 1);
262 controller.duration = const Duration(seconds: 1);
263 controller.forward();
264 expect(await tester.pumpAndSettle(const Duration(milliseconds: 300)), 5); // 0, 300, 600, 900, 1200ms
265 });
266
267 testWidgets('Input event array', (WidgetTester tester) async {
268 final List<String> logs = <String>[];
269
270 await tester.pumpWidget(
271 Directionality(
272 textDirection: TextDirection.ltr,
273 child: Listener(
274 onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}'),
275 onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}'),
276 onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}'),
277 child: const Text('test'),
278 ),
279 ),
280 );
281
282 final Offset location = tester.getCenter(find.text('test'));
283 final List<PointerEventRecord> records = <PointerEventRecord>[
284 PointerEventRecord(Duration.zero, <PointerEvent>[
285 // Typically PointerAddedEvent is not used in testers, but for records
286 // captured on a device it is usually what start a gesture.
287 PointerAddedEvent(
288 position: location,
289 ),
290 PointerDownEvent(
291 position: location,
292 buttons: kSecondaryMouseButton,
293 pointer: 1,
294 ),
295 ]),
296 ...<PointerEventRecord>[
297 for (Duration t = const Duration(milliseconds: 5);
298 t < const Duration(milliseconds: 80);
299 t += const Duration(milliseconds: 16))
300 PointerEventRecord(t, <PointerEvent>[
301 PointerMoveEvent(
302 timeStamp: t - const Duration(milliseconds: 1),
303 position: location,
304 buttons: kSecondaryMouseButton,
305 pointer: 1,
306 ),
307 ]),
308 ],
309 PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[
310 PointerUpEvent(
311 timeStamp: const Duration(milliseconds: 79),
312 position: location,
313 buttons: kSecondaryMouseButton,
314 pointer: 1,
315 ),
316 ]),
317 ];
318 final List<Duration> timeDiffs = await tester.handlePointerEventRecord(records);
319 expect(timeDiffs.length, records.length);
320 for (final Duration diff in timeDiffs) {
321 expect(diff, Duration.zero);
322 }
323
324 const String b = '$kSecondaryMouseButton';
325 expect(logs.first, 'down $b');
326 for (int i = 1; i < logs.length - 1; i++) {
327 expect(logs[i], 'move $b');
328 }
329 expect(logs.last, 'up $b');
330 });
331
332 group('runAsync', () {
333 testWidgets('works with no async calls', (WidgetTester tester) async {
334 String? value;
335 await tester.runAsync(() async {
336 value = '123';
337 });
338 expect(value, '123');
339 });
340
341 testWidgets('works with real async calls', (WidgetTester tester) async {
342 final StringBuffer buf = StringBuffer('1');
343 await tester.runAsync(() async {
344 buf.write('2');
345 //ignore: avoid_slow_async_io
346 await Directory.current.stat();
347 buf.write('3');
348 });
349 buf.write('4');
350 expect(buf.toString(), '1234');
351 });
352
353 testWidgets('propagates return values', (WidgetTester tester) async {
354 final String? value = await tester.runAsync<String>(() async {
355 return '123';
356 });
357 expect(value, '123');
358 });
359
360 testWidgets('reports errors via framework', (WidgetTester tester) async {
361 final String? value = await tester.runAsync<String>(() async {
362 throw ArgumentError();
363 });
364 expect(value, isNull);
365 expect(tester.takeException(), isArgumentError);
366 });
367
368 testWidgets('disallows re-entry', (WidgetTester tester) async {
369 final Completer<void> completer = Completer<void>();
370 tester.runAsync<void>(() => completer.future);
371 expect(() => tester.runAsync(() async { }), throwsA(isA<TestFailure>()));
372 completer.complete();
373 });
374
375 testWidgets('maintains existing zone values', (WidgetTester tester) async {
376 final Object key = Object();
377 await runZoned<Future<void>>(() {
378 expect(Zone.current[key], 'abczed');
379 return tester.runAsync<void>(() async {
380 expect(Zone.current[key], 'abczed');
381 });
382 }, zoneValues: <dynamic, dynamic>{
383 key: 'abczed',
384 });
385 });
386
387 testWidgets('control test (return value)', (WidgetTester tester) async {
388 final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner');
389 expect(result, 'Judy Turner');
390 });
391
392 testWidgets('async throw', (WidgetTester tester) async {
393 final String? result = await tester.binding.runAsync<Never>(() async => throw Exception('Lois Dilettente'));
394 expect(result, isNull);
395 expect(tester.takeException(), isNotNull);
396 });
397
398 testWidgets('sync throw', (WidgetTester tester) async {
399 final String? result = await tester.binding.runAsync<Never>(() => throw Exception('Butch Barton'));
400 expect(result, isNull);
401 expect(tester.takeException(), isNotNull);
402 });
403 });
404
405 group('showKeyboard', () {
406 testWidgets('can be called twice', (WidgetTester tester) async {
407 await tester.pumpWidget(
408 MaterialApp(
409 home: Material(
410 child: Center(
411 child: TextFormField(),
412 ),
413 ),
414 ),
415 );
416 await tester.showKeyboard(find.byType(TextField));
417 await tester.testTextInput.receiveAction(TextInputAction.done);
418 await tester.pump();
419 await tester.showKeyboard(find.byType(TextField));
420 await tester.testTextInput.receiveAction(TextInputAction.done);
421 await tester.pump();
422 await tester.showKeyboard(find.byType(TextField));
423 await tester.showKeyboard(find.byType(TextField));
424 await tester.pump();
425 });
426
427 testWidgets(
428 'can focus on offstage text input field if finder says not to skip offstage nodes',
429 (WidgetTester tester) async {
430 await tester.pumpWidget(
431 MaterialApp(
432 home: Material(
433 child: Offstage(
434 child: TextFormField(),
435 ),
436 ),
437 ),
438 );
439 await tester.showKeyboard(find.byType(TextField, skipOffstage: false));
440 });
441 });
442
443 testWidgets('verifyTickersWereDisposed control test', (WidgetTester tester) async {
444 late FlutterError error;
445 final Ticker ticker = tester.createTicker((Duration duration) {});
446 ticker.start();
447 try {
448 tester.verifyTickersWereDisposed('');
449 } on FlutterError catch (e) {
450 error = e;
451 } finally {
452 expect(error, isNotNull);
453 expect(error.diagnostics.length, 4);
454 expect(error.diagnostics[2].level, DiagnosticLevel.hint);
455 expect(
456 error.diagnostics[2].toStringDeep(),
457 'Tickers used by AnimationControllers should be disposed by\n'
458 'calling dispose() on the AnimationController itself. Otherwise,\n'
459 'the ticker will leak.\n',
460 );
461 expect(error.diagnostics.last, isA<DiagnosticsProperty<Ticker>>());
462 expect(error.diagnostics.last.value, ticker);
463 expect(error.toStringDeep(), startsWith(
464 'FlutterError\n'
465 ' A Ticker was active .\n'
466 ' All Tickers must be disposed.\n'
467 ' Tickers used by AnimationControllers should be disposed by\n'
468 ' calling dispose() on the AnimationController itself. Otherwise,\n'
469 ' the ticker will leak.\n'
470 ' The offending ticker was:\n'
471 ' _TestTicker()\n',
472 ));
473 }
474 ticker.stop();
475 });
476
477 group('testWidgets variants work', () {
478 int numberOfVariationsRun = 0;
479
480 testWidgets('variant tests run all values provided', (WidgetTester tester) async {
481 if (debugDefaultTargetPlatformOverride == null) {
482 expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
483 } else {
484 numberOfVariationsRun += 1;
485 }
486 }, variant: TargetPlatformVariant(TargetPlatform.values.toSet()));
487
488 testWidgets('variant tests have descriptions with details', (WidgetTester tester) async {
489 if (debugDefaultTargetPlatformOverride == null) {
490 expect(tester.testDescription, equals('variant tests have descriptions with details'));
491 } else {
492 expect(
493 tester.testDescription,
494 equals('variant tests have descriptions with details (variant: $debugDefaultTargetPlatformOverride)'),
495 );
496 }
497 }, variant: TargetPlatformVariant(TargetPlatform.values.toSet()));
498 });
499
500 group('TargetPlatformVariant', () {
501 int numberOfVariationsRun = 0;
502 TargetPlatform? origTargetPlatform;
503
504 setUpAll(() {
505 origTargetPlatform = debugDefaultTargetPlatformOverride;
506 });
507
508 tearDownAll(() {
509 expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform));
510 });
511
512 testWidgets('TargetPlatformVariant.only tests given value', (WidgetTester tester) async {
513 expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS));
514 expect(defaultTargetPlatform, equals(TargetPlatform.iOS));
515 }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
516
517 group('all', () {
518 testWidgets('TargetPlatformVariant.all tests run all variants', (WidgetTester tester) async {
519 if (debugDefaultTargetPlatformOverride == null) {
520 expect(numberOfVariationsRun, equals(TargetPlatform.values.length));
521 } else {
522 numberOfVariationsRun += 1;
523 }
524 }, variant: TargetPlatformVariant.all());
525
526 const Set<TargetPlatform> excludePlatforms = <TargetPlatform>{ TargetPlatform.android, TargetPlatform.linux };
527 testWidgets('TargetPlatformVariant.all, excluding runs an all variants except those provided in excluding', (WidgetTester tester) async {
528 if (debugDefaultTargetPlatformOverride == null) {
529 expect(numberOfVariationsRun, equals(TargetPlatform.values.length - excludePlatforms.length));
530 expect(
531 excludePlatforms,
532 isNot(contains(debugDefaultTargetPlatformOverride)),
533 reason: 'this test should not run on any platform in excludePlatforms'
534 );
535 } else {
536 numberOfVariationsRun += 1;
537 }
538 }, variant: TargetPlatformVariant.all(excluding: excludePlatforms));
539 });
540
541 testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values', (WidgetTester tester) async {
542 final TargetPlatformVariant all = TargetPlatformVariant.all();
543 final TargetPlatformVariant desktop = TargetPlatformVariant.all();
544 final TargetPlatformVariant mobile = TargetPlatformVariant.all();
545 expect(desktop.values.union(mobile.values), equals(all.values));
546 });
547 });
548
549 group('Pending timer', () {
550 late TestExceptionReporter currentExceptionReporter;
551 setUp(() {
552 currentExceptionReporter = reportTestException;
553 });
554
555 tearDown(() {
556 reportTestException = currentExceptionReporter;
557 });
558
559 test('Throws assertion message without code', () async {
560 late FlutterErrorDetails flutterErrorDetails;
561 reportTestException = (FlutterErrorDetails details, String testDescription) {
562 flutterErrorDetails = details;
563 };
564
565 final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
566 debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED');
567 await binding.runTest(() async {
568 final Timer timer = Timer(const Duration(seconds: 1), () {});
569 expect(timer.isActive, true);
570 }, () {});
571
572 expect(flutterErrorDetails.exception, isA<AssertionError>());
573 expect((flutterErrorDetails.exception as AssertionError).message, 'A Timer is still pending even after the widget tree was disposed.');
574 expect(binding.inTest, true);
575 binding.postTest();
576 });
577 });
578
579 group('Accessibility announcements testing API', () {
580 testWidgets('Returns the list of announcements', (WidgetTester tester) async {
581
582 // Make sure the handler is properly set
583 expect(TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
584 .checkMockMessageHandler(SystemChannels.accessibility.name, null), isFalse);
585
586 await SemanticsService.announce('announcement 1', TextDirection.ltr);
587 await SemanticsService.announce('announcement 2', TextDirection.rtl,
588 assertiveness: Assertiveness.assertive);
589 await SemanticsService.announce('announcement 3', TextDirection.rtl);
590
591 final List<CapturedAccessibilityAnnouncement> list = tester.takeAnnouncements();
592 expect(list, hasLength(3));
593 final CapturedAccessibilityAnnouncement first = list[0];
594 expect(first.message, 'announcement 1');
595 expect(first.textDirection, TextDirection.ltr);
596
597 final CapturedAccessibilityAnnouncement second = list[1];
598 expect(second.message, 'announcement 2');
599 expect(second.textDirection, TextDirection.rtl);
600 expect(second.assertiveness, Assertiveness.assertive);
601
602 final CapturedAccessibilityAnnouncement third = list[2];
603 expect(third.message, 'announcement 3');
604 expect(third.textDirection, TextDirection.rtl);
605 expect(third.assertiveness, Assertiveness.polite);
606
607 final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements();
608 expect(emptyList, <CapturedAccessibilityAnnouncement>[]);
609 });
610
611 test('New test API is not breaking existing tests', () async {
612 final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[];
613
614 Future<dynamic> handleMessage(dynamic mockMessage) async {
615 final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>;
616 log.add(message);
617 }
618
619 TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
620 .setMockDecodedMessageHandler<dynamic>(
621 SystemChannels.accessibility, handleMessage);
622
623 await SemanticsService.announce('announcement 1', TextDirection.rtl,
624 assertiveness: Assertiveness.assertive);
625 expect(
626 log,
627 equals(<Map<String, dynamic>>[
628 <String, dynamic>{
629 'type': 'announce',
630 'data': <String, dynamic>{
631 'message': 'announcement 1',
632 'textDirection': 0,
633 'assertiveness': 1
634 }
635 },
636 ]));
637
638 // Remove the handler
639 TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
640 .setMockDecodedMessageHandler<dynamic>(
641 SystemChannels.accessibility, null);
642 });
643
644 tearDown(() {
645 // Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest]
646 expect(TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
647 .checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue);
648 });
649 });
650
651 testWidgets('wrapWithView: false does not include View', (WidgetTester tester) async {
652 FlutterView? flutterView;
653 View? view;
654 int builderCount = 0;
655 await tester.pumpWidget(
656 wrapWithView: false,
657 Builder(
658 builder: (BuildContext context) {
659 builderCount++;
660 flutterView = View.maybeOf(context);
661 view = context.findAncestorWidgetOfExactType<View>();
662 return const ViewCollection(views: <Widget>[]);
663 },
664 ),
665 );
666
667 expect(builderCount, 1);
668 expect(view, isNull);
669 expect(flutterView, isNull);
670 expect(find.byType(View), findsNothing);
671 });
672
673 testWidgets('passing a view to pumpWidget with wrapWithView: true throws', (WidgetTester tester) async {
674 await tester.pumpWidget(
675 View(
676 view: FakeView(tester.view),
677 child: const SizedBox.shrink(),
678 ),
679 );
680 expect(
681 tester.takeException(),
682 isFlutterError.having(
683 (FlutterError e) => e.message,
684 'message',
685 contains('consider setting the "wrapWithView" parameter of that method to false'),
686 ),
687 );
688 });
689
690 testWidgets('can pass a View to pumpWidget when wrapWithView: false', (WidgetTester tester) async {
691 await tester.pumpWidget(
692 wrapWithView: false,
693 View(
694 view: tester.view,
695 child: const SizedBox.shrink(),
696 ),
697 );
698 expect(find.byType(View), findsOne);
699 });
700}
701
702class FakeMatcher extends AsyncMatcher {
703 FakeMatcher(this.completer);
704
705 final Completer<void> completer;
706
707 @override
708 Future<String?> matchAsync(dynamic object) {
709 return completer.future.then<String?>((void value) {
710 return object?.toString();
711 });
712 }
713
714 @override
715 Description describe(Description description) => description.add('--fake--');
716}
717
718class _AlwaysAnimating extends StatefulWidget {
719 const _AlwaysAnimating({
720 required this.onPaint,
721 });
722
723 final VoidCallback onPaint;
724
725 @override
726 State<StatefulWidget> createState() => _AlwaysAnimatingState();
727}
728
729class _AlwaysAnimatingState extends State<_AlwaysAnimating> with SingleTickerProviderStateMixin {
730 late AnimationController _controller;
731
732 @override
733 void initState() {
734 super.initState();
735 _controller = AnimationController(
736 duration: const Duration(milliseconds: 100),
737 vsync: this,
738 );
739 _controller.repeat();
740 }
741
742 @override
743 void dispose() {
744 _controller.dispose();
745 super.dispose();
746 }
747
748 @override
749 Widget build(BuildContext context) {
750 return AnimatedBuilder(
751 animation: _controller.view,
752 builder: (BuildContext context, Widget? child) {
753 return CustomPaint(
754 painter: _AlwaysRepaint(widget.onPaint),
755 );
756 },
757 );
758 }
759}
760
761class _AlwaysRepaint extends CustomPainter {
762 _AlwaysRepaint(this.onPaint);
763
764 final VoidCallback onPaint;
765
766 @override
767 bool shouldRepaint(CustomPainter oldDelegate) => true;
768
769 @override
770 void paint(Canvas canvas, Size size) {
771 onPaint();
772 }
773}
774

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com